taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 276678907296fd0ed93f284c5ecb8a98361f652b
parent 324c40b3a38df93ca98413964954210509b22fd1
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Thu, 14 May 2026 16:37:17 -0300

fix #8989 better pagination

Diffstat:
Mpackages/aml-backoffice-ui/src/hooks/decisions.ts | 62+++++++++++++++++++++++++++++++++-----------------------------
Mpackages/aml-backoffice-ui/src/hooks/legitimizations.ts | 28+++++++++++++++++-----------
Mpackages/aml-backoffice-ui/src/hooks/transfers.ts | 45+++++++++++++++++++++++++--------------------
Mpackages/merchant-backoffice-ui/src/components/form/InputStock.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/hooks/access-tokens.ts | 24++++++++++++++----------
Mpackages/merchant-backoffice-ui/src/hooks/bank.ts | 5-----
Mpackages/merchant-backoffice-ui/src/hooks/category.ts | 11+++++------
Mpackages/merchant-backoffice-ui/src/hooks/groups.ts | 15++++++---------
Mpackages/merchant-backoffice-ui/src/hooks/order.test.ts | 24++++++++++++------------
Mpackages/merchant-backoffice-ui/src/hooks/order.ts | 24+++++++++++++-----------
Mpackages/merchant-backoffice-ui/src/hooks/otp.ts | 17++++++++---------
Mpackages/merchant-backoffice-ui/src/hooks/pots.ts | 5-----
Mpackages/merchant-backoffice-ui/src/hooks/product.ts | 42+++++++++++++++++++-----------------------
Mpackages/merchant-backoffice-ui/src/hooks/reports.ts | 22+++++++++++-----------
Mpackages/merchant-backoffice-ui/src/hooks/templates.ts | 25+++++++++----------------
Mpackages/merchant-backoffice-ui/src/hooks/tokenfamily.ts | 18+++++++++---------
Mpackages/merchant-backoffice-ui/src/hooks/transfer.test.ts | 8+++-----
Mpackages/merchant-backoffice-ui/src/hooks/transfer.ts | 46+++++++++++++++++++++++++++-------------------
Mpackages/merchant-backoffice-ui/src/hooks/webhooks.ts | 9+--------
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/Table.tsx | 42+++++++++---------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx | 3+--
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx | 41+++++++++--------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx | 3+--
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx | 33+++++++++++++--------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx | 52+++++++++-------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx | 3+--
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx | 11++++-------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx | 86+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 12+++---------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx | 41+++++++++--------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx | 9+++++----
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx | 41+++++++++--------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx | 3+--
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 39+++++++++------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx | 7+++----
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx | 41+++++++++--------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx | 11++++++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/Table.tsx | 8+++++---
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx | 45++++++++++++---------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx | 3+--
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx | 8++------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx | 74+++++++++++---------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 5-----
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx | 14+++++++-------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx | 41+++++++++--------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx | 6++----
Mpackages/merchant-backoffice-ui/src/utils/constants.ts | 3++-
Mpackages/web-util/src/utils/buildPaginatedResult.ts | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
48 files changed, 523 insertions(+), 718 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -17,14 +17,18 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { + AmlDecision, + CustomerAccountSummary, OfficerSession, opFixedSuccess, TalerExchangeResultByMethod2, - TalerHttpError + TalerHttpError, } from "@gnu-taler/taler-util"; import { buildPaginatedResult, + ListPointer, useExchangeApiContext, + useListPointer, } from "@gnu-taler/web-util/browser"; import _useSWR, { mutate, SWRHook } from "swr"; import { useOfficer } from "./officer.js"; @@ -55,18 +59,20 @@ export function useAmlAccounts({ lib: { exchange: api }, } = useExchangeApiContext(); - const [offset, setOffset] = useState<string>(); + const [pointer, updatePointer] = useListPointer( + (row: CustomerAccountSummary) => String(row.rowid), + ); async function fetcher([officer, offset, investigation, highRisk, open]: [ OfficerSession, - string | undefined, + ListPointer, boolean | undefined, boolean | undefined, boolean | undefined, ]) { return await api.getAmlAccounts(officer, { - order: "dec", - offset, + order: offset.order, + offset: offset.id, limit: PAGINATED_LIST_REQUEST, investigation, highRisk, @@ -80,7 +86,7 @@ export function useAmlAccounts({ >( !session ? undefined - : [session, offset, investigated, highRisk, open, "getAmlAccounts"], + : [session, pointer, investigated, highRisk, open, "getAmlAccounts"], fetcher, ); @@ -90,9 +96,8 @@ export function useAmlAccounts({ return buildPaginatedResult( data.body.accounts, - offset, - setOffset, - (d) => String(d.rowid), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } @@ -113,16 +118,18 @@ export function useCurrentDecisions({ lib: { exchange: api }, } = useExchangeApiContext(); - const [offset, setOffset] = useState<string>(); + const [pointer, updatePointer] = useListPointer((row: AmlDecision) => + String(row.rowid), + ); async function fetcher([officer, offset, investigation]: [ OfficerSession, - string | undefined, + ListPointer, boolean | undefined, ]) { return await api.getAmlDecisions(officer, { - order: "dec", - offset, + order: offset.order, + offset: offset.id, active: true, investigation, limit: PAGINATED_LIST_REQUEST, @@ -133,7 +140,7 @@ export function useCurrentDecisions({ TalerExchangeResultByMethod2<"getAmlDecisions">, TalerHttpError >( - !session ? undefined : [session, offset, investigated, "getAmlDecisions"], + !session ? undefined : [session, pointer, investigated, "getAmlDecisions"], fetcher, ); @@ -143,9 +150,8 @@ export function useCurrentDecisions({ return buildPaginatedResult( data.body.records, - offset, - setOffset, - (d) => String(d.rowid), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } @@ -169,16 +175,18 @@ export function useAccountDecisions(accountStr: string) { lib: { exchange: api }, } = useExchangeApiContext(); - const [offset, setOffset] = useState<string>(); + const [pointer, updatePointer] = useListPointer((row: AmlDecision) => + String(row.rowid), + ); async function fetcher([officer, account, offset]: [ OfficerSession, string, - string | undefined, + ListPointer, ]) { return await api.getAmlDecisions(officer, { - order: "dec", - offset, + order: offset.order, + offset: offset.id, account, limit: PAGINATED_LIST_REQUEST, }); @@ -188,7 +196,7 @@ export function useAccountDecisions(accountStr: string) { TalerExchangeResultByMethod2<"getAmlDecisions">, TalerHttpError >( - !session ? undefined : [session, accountStr, offset, "getAmlDecisions"], + !session ? undefined : [session, accountStr, pointer, "getAmlDecisions"], fetcher, ); @@ -198,9 +206,8 @@ export function useAccountDecisions(accountStr: string) { return buildPaginatedResult( data.body.records, - offset, - setOffset, - (d) => String(d.rowid), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } @@ -238,10 +245,7 @@ export function useAccountActiveDecision(accountStr?: string) { const { data, error } = useSWR< TalerExchangeResultByMethod2<"getAmlDecisions">, TalerHttpError - >( - !session ? undefined : [session, accountStr, "getAmlDecisions"], - fetcher, - ); + >(!session ? undefined : [session, accountStr, "getAmlDecisions"], fetcher); if (error) return error; if (data === undefined) return undefined; diff --git a/packages/aml-backoffice-ui/src/hooks/legitimizations.ts b/packages/aml-backoffice-ui/src/hooks/legitimizations.ts @@ -13,18 +13,19 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { + LegitimizationMeasureDetails, OfficerSession, - opFixedSuccess, TalerExchangeResultByMethod2, TalerHttpError, } from "@gnu-taler/taler-util"; import { buildPaginatedResult, + ListPointer, useExchangeApiContext, + useListPointer, } from "@gnu-taler/web-util/browser"; import _useSWR, { mutate, SWRHook } from "swr"; import { useOfficer } from "./officer.js"; @@ -56,17 +57,18 @@ export function useCurrentLegitimizations(accoutnStr: string) { lib: { exchange: api }, } = useExchangeApiContext(); - const [offset, setOffset] = useState<string>(); + const [pointer, updatePointer] = useListPointer( + (row: LegitimizationMeasureDetails) => String(row.rowid), + ); async function fetcher([officer, account, offset]: [ OfficerSession, string | undefined, - string | undefined, - boolean | undefined, + ListPointer, ]) { return await api.getAmlLegitimizations(officer, { - order: "dec", - offset, + order: offset.order, + offset: offset.id, active: true, account, limit: PAGINATED_LIST_REQUEST, @@ -76,7 +78,12 @@ export function useCurrentLegitimizations(accoutnStr: string) { const { data, error } = useSWR< TalerExchangeResultByMethod2<"getAmlLegitimizations">, TalerHttpError - >(!session ? undefined : [session, accoutnStr, offset, "getAmlLegitimizations"], fetcher); + >( + !session + ? undefined + : [session, accoutnStr, pointer, "getAmlLegitimizations"], + fetcher, + ); if (error) return error; if (data === undefined) return undefined; @@ -84,9 +91,8 @@ export function useCurrentLegitimizations(accoutnStr: string) { return buildPaginatedResult( data.body.measures, - offset, - setOffset, - (d) => String(d.rowid), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } diff --git a/packages/aml-backoffice-ui/src/hooks/transfers.ts b/packages/aml-backoffice-ui/src/hooks/transfers.ts @@ -18,6 +18,7 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { AmountJson, + ExchangeTransferListEntry, OfficerSession, PaytoHash, TalerExchangeResultByMethod2, @@ -25,7 +26,9 @@ import { } from "@gnu-taler/taler-util"; import { buildPaginatedResult, + ListPointer, useExchangeApiContext, + useListPointer, } from "@gnu-taler/web-util/browser"; import _useSWR, { mutate, SWRHook } from "swr"; import { useOfficer } from "./officer.js"; @@ -57,16 +60,18 @@ export function useTransferDebit() { lib: { exchange: api }, } = useExchangeApiContext(); - const [offset, setOffset] = useState<string>(); + const [pointer, updatePointer] = useListPointer( + (row: ExchangeTransferListEntry) => String(row.rowid), + ); async function fetcher([officer, offset]: [ OfficerSession, - string, + ListPointer, string | undefined, ]) { return await api.getTransfersDebit(officer, { - order: "dec", - offset, + offset: offset?.id, + order: offset?.order, limit: PAGINATED_LIST_REQUEST, }); } @@ -74,7 +79,7 @@ export function useTransferDebit() { const { data, error } = useSWR< TalerExchangeResultByMethod2<"getTransfersDebit">, TalerHttpError - >(!session ? undefined : [session, offset, "getTransfersDebit"], fetcher); + >(!session ? undefined : [session, pointer, "getTransfersDebit"], fetcher); if (error) return error; if (data === undefined) return undefined; @@ -82,9 +87,8 @@ export function useTransferDebit() { return buildPaginatedResult( data.body.transfers, - offset, - setOffset, - (d) => String(d.rowid), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } @@ -109,11 +113,13 @@ export function useTransferList({ lib: { exchange: api }, } = useExchangeApiContext(); - const [offset, setOffset] = useState<string>(); + const [pointer, updatePointer] = useListPointer( + (row: ExchangeTransferListEntry) => String(row.rowid), + ); async function fetcher([officer, offset, direction, threshold, account]: [ OfficerSession, - string, + ListPointer, Direction, AmountJson | undefined, PaytoHash | undefined, @@ -121,8 +127,8 @@ export function useTransferList({ switch (direction) { case "credit": { return await api.getTransfersCredit(officer, { - order: "dec", - offset, + order: offset.order, + offset: offset.id, limit: PAGINATED_LIST_REQUEST, threshold, account, @@ -130,8 +136,8 @@ export function useTransferList({ } case "debit": { return await api.getTransfersDebit(officer, { - order: "dec", - offset, + order: offset.order, + offset: offset.id, limit: PAGINATED_LIST_REQUEST, threshold, account, @@ -139,8 +145,8 @@ export function useTransferList({ } case "kyc-auth": { return await api.getTransfersKycAuth(officer, { - order: "dec", - offset, + order: offset.order, + offset: offset.id, limit: PAGINATED_LIST_REQUEST, threshold, account, @@ -157,7 +163,7 @@ export function useTransferList({ ? undefined : [ session, - offset, + pointer, direction ?? "debit", threshold, account, @@ -172,9 +178,8 @@ export function useTransferList({ return buildPaginatedResult( data.body.transfers, - offset, - setOffset, - (d) => String(d.rowid), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx @@ -69,7 +69,7 @@ export function InputStock<T>({ const [addedStock, setAddedStock] = useState<StockDelta>({ incoming: 0, lost: 0, - current: formValue.current, + current: formValue?.current, }); const { i18n } = useTranslationContext(); diff --git a/packages/merchant-backoffice-ui/src/hooks/access-tokens.ts b/packages/merchant-backoffice-ui/src/hooks/access-tokens.ts @@ -19,9 +19,9 @@ import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod, + TokenInfo, } from "@gnu-taler/taler-util"; -import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; -import { useState } from "preact/hooks"; +import { buildPaginatedResult, ListPointer, useListPointer } from "@gnu-taler/web-util/browser"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; @@ -37,23 +37,28 @@ export function revalidateInstanceAccessTokens() { { revalidate: true }, ); } + + export function useInstanceAccessTokens() { const { state, lib } = useSessionContext(); - const [offset, setOffset] = useState<number | undefined>(); + const [pointer, setPointer] = useListPointer((row:TokenInfo) => String(row.serial)) - async function fetcher([token, tid]: [AccessToken, number]) { + async function fetcher([token, pointer]: [ + AccessToken, + ListPointer | undefined, + ]) { return await lib.instance.listAccessTokens(token, { limit: PAGINATED_LIST_REQUEST, - offset: !tid? undefined :String(tid), - order: "dec", + offset: pointer?.id, + order: pointer?.order, }); } const { data, error } = useSWR< TalerMerchantManagementResultByMethod<"listAccessTokens">, TalerHttpError - >([state.token, offset, "useInstanceAccessTokens"], fetcher); + >([state.token, pointer, "useInstanceAccessTokens"], fetcher); if (error) return error; if (data === undefined) return undefined; @@ -61,9 +66,8 @@ export function useInstanceAccessTokens() { return buildPaginatedResult( data.body.tokens, - offset, - setOffset, - (d) => d.serial, + pointer, + setPointer, PAGINATED_LIST_REQUEST, ); } diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts @@ -36,13 +36,8 @@ export function revalidateInstanceBankAccounts() { export function useInstanceBankAccounts() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(); - async function fetcher([token, _bid]: [AccessToken, string]) { return await lib.instance.listBankAccounts(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid, - // order: "dec", }); } diff --git a/packages/merchant-backoffice-ui/src/hooks/category.ts b/packages/merchant-backoffice-ui/src/hooks/category.ts @@ -15,7 +15,11 @@ */ // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -30,13 +34,8 @@ export function revalidateInstanceCategories() { export function useInstanceCategories() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(); - async function fetcher([token, _bid]: [AccessToken, string]) { return await lib.instance.listCategories(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid, - // order: "dec", }); } diff --git a/packages/merchant-backoffice-ui/src/hooks/groups.ts b/packages/merchant-backoffice-ui/src/hooks/groups.ts @@ -15,10 +15,13 @@ */ // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; -import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; const useSWR = _useSWR as unknown as SWRHook; export function revalidateInstanceGroups() { @@ -31,14 +34,8 @@ export function revalidateInstanceGroups() { export function useInstanceProductGroups() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(); - async function fetcher([token, bid]: [AccessToken, string]) { - return await lib.instance.listProductGroups(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid, - // order: "dec", - }); + return await lib.instance.listProductGroups(token, {}); } const { data, error } = useSWR< diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts @@ -59,7 +59,7 @@ describe("order api interaction with listing", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: true }, newDate); + const query = useInstanceOrders({ paid: true }); const { lib: api } = useMerchantApiContext(); return { query, api }; }, @@ -143,7 +143,7 @@ describe("order api interaction with listing", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: true }, newDate); + const query = useInstanceOrders({ paid: true }); const { lib: api } = useMerchantApiContext(); return { query, api }; }, @@ -235,7 +235,7 @@ describe("order api interaction with listing", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: true }, newDate); + const query = useInstanceOrders({ paid: true }); const { lib: api } = useMerchantApiContext(); return { query, api }; }, @@ -450,10 +450,10 @@ describe("order listing pagination", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { const date = new Date(12000); - const query = useInstanceOrders( - { wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, - newDate, - ); + const query = useInstanceOrders({ + wired: true, + date: AbsoluteTime.fromMilliseconds(date.getTime()), + }); const { lib: api } = useMerchantApiContext(); return { query, api }; }, @@ -483,7 +483,7 @@ describe("order listing pagination", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); }); - it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => { + it("should load more if result brings more that PAGINATED_LISTREQUEST", async () => { const env = new ApiMockEnvironment(); const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ @@ -514,10 +514,10 @@ describe("order listing pagination", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { const date = new Date(12000); - const query = useInstanceOrders( - { wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, - newDate, - ); + const query = useInstanceOrders({ + wired: true, + date: AbsoluteTime.fromMilliseconds(date.getTime()), + }); const { lib: api } = useMerchantApiContext(); return { query, api }; }, diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -21,6 +21,7 @@ import { AccessToken, ListOrdersRequestParams, opFixedSuccess, + OrderHistoryEntry, TalerError, TalerHttpError, TalerMerchantManagementResultByMethod, @@ -28,6 +29,7 @@ import { import { buildPaginatedResult, LONG_POLL_DELAY, + useListPointer, useLongPolling, } from "@gnu-taler/web-util/browser"; import _useSWR, { mutate, SWRHook } from "swr"; @@ -113,15 +115,16 @@ export function revalidateInstanceOrders() { { revalidate: true }, ); } -export function useInstanceOrders( - args?: InstanceOrderFilter, - updatePosition: (d: string | undefined) => void = () => {}, -) { +export function useInstanceOrders(args?: InstanceOrderFilter) { const { state, lib } = useSessionContext(); const token = state.token!; + const [pointer, updatePointer] = useListPointer((row: OrderHistoryEntry) => + String(row.row_id), + ); + const cacheKey = [ - args?.position, + pointer, args?.paid, args?.refunded, args?.wired, @@ -129,11 +132,11 @@ export function useInstanceOrders( "listOrders", ]; - async function fetcher([position, paid, refunded, wired, date]: any) { + async function fetcher([pointer, paid, refunded, wired, date]: any) { return await lib.instance.listOrders(token, { limit: PAGINATED_LIST_REQUEST, - order: "dec", - offset: position, + offset: pointer?.id, + order: pointer?.order, paid: paid, refunded: refunded, wired: wired, @@ -188,9 +191,8 @@ export function useInstanceOrders( return buildPaginatedResult( result.body.orders, - args?.position, - updatePosition, - (d) => String(d.row_id), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts @@ -15,7 +15,11 @@ */ // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -30,14 +34,8 @@ export function revalidateInstanceOtpDevices() { export function useInstanceOtpDevices() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(); - async function fetcher([token, _bid]: [AccessToken, string]) { - return await lib.instance.listOtpDevices(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid, - // order: "dec", - }); + return await lib.instance.listOtpDevices(token, {}); } const { data, error } = useSWR< @@ -55,7 +53,8 @@ export function useInstanceOtpDevices() { export function revalidateOtpDeviceDetails() { return mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "getOtpDeviceDetails", + (key) => + Array.isArray(key) && key[key.length - 1] === "getOtpDeviceDetails", undefined, { revalidate: true }, ); diff --git a/packages/merchant-backoffice-ui/src/hooks/pots.ts b/packages/merchant-backoffice-ui/src/hooks/pots.ts @@ -30,13 +30,8 @@ export function revalidateInstanceMoneyPots() { export function useInstanceMoneyPots() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(); - async function fetcher([token, _bid]: [AccessToken, string]) { return await lib.instance.listMoneyPots(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid, - // order: "dec", }); } diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -24,11 +24,14 @@ import { TalerMerchantManagementResultByMethod, opFixedSuccess, } from "@gnu-taler/taler-util"; -import { useState } from "preact/hooks"; +import { + ListPointer, + buildPaginatedResult, + useListPointer, +} from "@gnu-taler/web-util/browser"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; -import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; export type ProductWithId = TalerMerchantApi.ProductDetailResponse & { @@ -49,13 +52,15 @@ export function revalidateInstanceProducts() { export function useInstanceProducts() { const { state, lib } = useSessionContext(); - const [offset, setOffset] = useState<number | undefined>(); + const [pointer, updatePointer] = useListPointer((row: ProductWithId) => + String(row.serial), + ); - async function fetcher([token, bid]: [AccessToken, number]) { + async function fetcher([token, pointer]: [AccessToken, ListPointer]) { const list = await lib.instance.listProducts(token, { limit: PAGINATED_LIST_REQUEST, - offset: bid === undefined ? undefined : String(bid), - order: "dec", + offset: pointer?.id, + order: pointer?.order, }); if (list.type !== "ok") { return list; @@ -78,7 +83,7 @@ export function useInstanceProducts() { | OperationOk<{ products: ProductWithId[] }> | TalerMerchantManagementErrorsByMethod<"listProducts">, TalerHttpError - >([state.token, offset, "listProductsWithId"], fetcher); + >([state.token, pointer, "listProductsWithId"], fetcher); if (error) return error; if (data === undefined) return undefined; @@ -86,9 +91,8 @@ export function useInstanceProducts() { return buildPaginatedResult( data.body.products, - offset, - setOffset, - (d) => d.serial, + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } @@ -96,9 +100,7 @@ export function useInstanceProducts() { export function useInstanceProductsFromIds(ids: string[]) { const { state, lib } = useSessionContext(); - const [offset, setOffset] = useState<number | undefined>(); - - async function fetcher([token, bid]: [AccessToken, number]) { + async function fetcher([token]: [AccessToken]) { const all: Array<ProductWithId | undefined> = await Promise.all( ids.map(async (id, idx) => { const r = await lib.instance.getProductDetails(token, id); @@ -110,14 +112,14 @@ export function useInstanceProductsFromIds(ids: string[]) { ); const products = all.filter(notUndefined); - return opFixedSuccess({ products }); + return opFixedSuccess(products); } const { data, error } = useSWR< - | OperationOk<{ products: ProductWithId[] }> + | OperationOk<ProductWithId[]> | TalerMerchantManagementErrorsByMethod<"listProducts">, TalerHttpError - >([state.token, offset], fetcher, { + >([state.token], fetcher, { revalidateOnFocus: true, revalidateIfStale: true, revalidateOnMount: true, @@ -134,13 +136,7 @@ export function useInstanceProductsFromIds(ids: string[]) { if (data === undefined) return undefined; if (data.type !== "ok") return data; - return buildPaginatedResult( - data.body.products, - offset, - setOffset, - (d) => d.serial, - PAGINATED_LIST_REQUEST, - ); + return data; } export function revalidateProductDetails() { diff --git a/packages/merchant-backoffice-ui/src/hooks/reports.ts b/packages/merchant-backoffice-ui/src/hooks/reports.ts @@ -15,14 +15,19 @@ */ // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, opFixedSuccess, ReportsSummaryResponse, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; -import _useSWR, { SWRHook, mutate } from "swr"; +import { + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; +import _useSWR, { mutate, SWRHook } from "swr"; import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; export function revalidateInstanceScheduledReports() { return mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "listScheduledReports", + (key) => + Array.isArray(key) && key[key.length - 1] === "listScheduledReports", undefined, { revalidate: true }, ); @@ -30,14 +35,8 @@ export function revalidateInstanceScheduledReports() { export function useInstanceScheduledReports() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(); - async function fetcher([token, _bid]: [AccessToken, string]) { - return await lib.instance.listScheduledReports(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid, - // order: "dec", - }); + return await lib.instance.listScheduledReports(token, {}); } const { data, error } = useSWR< @@ -55,7 +54,8 @@ export function useInstanceScheduledReports() { export function revalidateScheduledReportDetails() { return mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "getScheduledReportDetails", + (key) => + Array.isArray(key) && key[key.length - 1] === "getScheduledReportDetails", undefined, { revalidate: true }, ); diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -13,18 +13,18 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { useState } from "preact/hooks"; -import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AccessToken, + TalerHttpError, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; - -export interface InstanceTemplateFilter { -} +export interface InstanceTemplateFilter {} export function revalidateInstanceTemplates() { return mutate( @@ -36,20 +36,14 @@ export function revalidateInstanceTemplates() { export function useInstanceTemplates() { const { state, lib } = useSessionContext(); - const [offset] = useState<string | undefined>(); - - async function fetcher([token, bid]: [AccessToken, string]) { - return await lib.instance.listTemplates(token, { - limit: PAGINATED_LIST_REQUEST, - offset: bid, - order: "dec", - }); + async function fetcher([token]: [AccessToken]) { + return await lib.instance.listTemplates(token); } const { data, error } = useSWR< TalerMerchantManagementResultByMethod<"listTemplates">, TalerHttpError - >([state.token, offset, "listTemplates"], fetcher); + >([state.token, "listTemplates"], fetcher); if (error) return error; if (data === undefined) return undefined; @@ -57,7 +51,6 @@ export function useInstanceTemplates() { // return buildPaginatedResult(data.body.templates, offset, setOffset, (d) => d.template_id) return data; - } export function revalidateTemplateDetails() { diff --git a/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts @@ -16,7 +16,12 @@ import { useSessionContext } from "../context/session.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, TalerHttpError, TalerMerchantApi, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import { + AccessToken, + TalerHttpError, + TalerMerchantApi, + TalerMerchantManagementResultByMethod, +} from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; const useSWR = _useSWR as unknown as SWRHook; @@ -30,14 +35,8 @@ export function revalidateTokenFamilies() { export function useInstanceTokenFamilies() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<number | undefined>(); - async function fetcher([token, _bid]: [AccessToken, number]) { - return await lib.instance.listTokenFamilies(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid === undefined ? undefined: String(bid), - // order: "dec", - }); + return await lib.instance.listTokenFamilies(token, {}); } const { data, error } = useSWR< @@ -54,7 +53,8 @@ export function useInstanceTokenFamilies() { export function revalidateTokenFamilyDetails() { return mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "getTokenFamilyDetails", + (key) => + Array.isArray(key) && key[key.length - 1] === "getTokenFamilyDetails", undefined, { revalidate: true }, ); diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts @@ -48,7 +48,7 @@ describe("transfer api interaction with listing", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceConfirmedTransfers({}, moveCursor); + const query = useInstanceConfirmedTransfers({}); const { lib: api } = useMerchantApiContext(); return { query, api }; }, @@ -132,7 +132,6 @@ describe("transfer listing pagination", () => { () => { const query = useInstanceConfirmedTransfers( { payto_uri: "payto://" }, - moveCursor, ); return { query }; }, @@ -163,7 +162,7 @@ describe("transfer listing pagination", () => { expect(hookBehavior).deep.eq({ result: "ok" }); }); - it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => { + it("should load more if result brings more that PAGINATED_LIST_REQEST", async () => { const env = new ApiMockEnvironment(); const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ @@ -195,8 +194,7 @@ describe("transfer listing pagination", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceConfirmedTransfers( - { payto_uri: "payto://", position: "1" }, - moveCursor, + { payto_uri: "payto://" }, ); return { query }; }, diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -18,24 +18,28 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { AccessToken, + ExpectedTransferEntry, TalerHttpError, TalerMerchantManagementResultByMethod, + TransferDetails, } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; -import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; +import { + buildPaginatedResult, + ListPointer, + useListPointer, +} from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; export interface InstanceIncomingTransferFilter { payto_uri?: string; verified?: boolean; confirmed?: boolean; - position?: string; } export interface InstanceConfirmedTransferFilter { payto_uri?: string; expected?: boolean; - position?: string; verified?: boolean; } @@ -50,13 +54,16 @@ export function revalidateInstanceIncomingTransfers() { export function useInstanceIncomingTransfers( args?: InstanceIncomingTransferFilter, - updatePosition: (id: string | undefined) => void = () => {}, ) { const { state, lib } = useSessionContext(); + const [pointer, updatePointer] = useListPointer( + (row: ExpectedTransferEntry) => String(row.expected_transfer_serial_id), + ); + async function fetcher([token, o, p, v, c]: [ AccessToken, - string, + ListPointer, string, boolean, boolean, @@ -66,8 +73,8 @@ export function useInstanceIncomingTransfers( verified: v, confirmed: c, limit: PAGINATED_LIST_REQUEST, - offset: o, - order: "dec", + offset: o.id, + order: o.order, }); } @@ -77,7 +84,7 @@ export function useInstanceIncomingTransfers( >( [ state.token, - args?.position, + pointer, args?.payto_uri, args?.verified, args?.confirmed, @@ -92,9 +99,8 @@ export function useInstanceIncomingTransfers( return buildPaginatedResult( data.body.incoming, - args?.position, - updatePosition, - (d) => String(d.expected_transfer_serial_id), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } @@ -144,13 +150,16 @@ export function revalidateInstanceConfirmedTransfers() { } export function useInstanceConfirmedTransfers( args?: InstanceConfirmedTransferFilter, - updatePosition: (id: string | undefined) => void = () => {}, ) { const { state, lib } = useSessionContext(); + const [pointer, updatePointer] = useListPointer((row: TransferDetails) => + String(row.transfer_serial_id), + ); + async function fetcher([token, o, p, e]: [ AccessToken, - string, + ListPointer, string, boolean, ]) { @@ -158,8 +167,8 @@ export function useInstanceConfirmedTransfers( paytoURI: p, expected: e, limit: PAGINATED_LIST_REQUEST, - offset: o, - order: "dec", + offset: o.id, + order: o.order, }); } @@ -169,7 +178,7 @@ export function useInstanceConfirmedTransfers( >( [ state.token, - args?.position, + pointer, args?.payto_uri, args?.expected, "listConfirmedWireTransfers", @@ -183,9 +192,8 @@ export function useInstanceConfirmedTransfers( return buildPaginatedResult( data.body.transfers, - args?.position, - updatePosition, - (d) => String(d.transfer_serial_id), + pointer, + updatePointer, PAGINATED_LIST_REQUEST, ); } diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -13,14 +13,12 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { AccessToken, - OperationOk, TalerHttpError, - TalerMerchantManagementResultByMethod, + TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; @@ -38,13 +36,8 @@ export function revalidateInstanceWebhooks() { export function useInstanceWebhooks() { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(); - async function fetcher([token, _bid]: [AccessToken, string]) { return await lib.instance.listWebhooks(token, { - // limit: PAGINATED_LIST_REQUEST, - // offset: bid, - // order: "dec", }); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/Table.tsx @@ -20,7 +20,7 @@ */ import { TokenInfo } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { PaginationControl, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; @@ -29,6 +29,7 @@ import { usePreference, } from "../../../../hooks/preference.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 32; @@ -38,16 +39,14 @@ interface Props { tokens: Entity[]; onDelete: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ tokens, onCreate, onDelete, - onLoadMoreAfter, - onLoadMoreBefore, + paginator }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -82,8 +81,7 @@ export function CardTable({ onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -99,32 +97,20 @@ interface TableProps { tokens: Entity[]; onDelete: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ tokens, onDelete, - onLoadMoreAfter, - onLoadMoreBefore, + paginator }: TableProps): VNode { const { i18n } = useTranslationContext(); const [preferences] = usePreference(); return ( <Fragment> <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more devices before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -200,17 +186,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more devices after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx @@ -118,8 +118,7 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { ...o, id: String(o.serial), }))} - onLoadMoreBefore={result.loadFirst} - onLoadMoreAfter={result.loadNext} + paginator={result} onCreate={onCreate} onDelete={(d) => { setDeleting(d); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx @@ -27,6 +27,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, SafeHandlerTemplate, useLocalNotificationBetter, useTranslationContext, @@ -35,6 +36,7 @@ import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 103; @@ -44,16 +46,14 @@ interface Props { devices: Entity[]; onSelect: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ devices, onCreate, onSelect, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -109,8 +109,7 @@ export function CardTable({ onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -128,31 +127,19 @@ interface TableProps { onDelete: SafeHandlerTemplate<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ instances, - onLoadMoreAfter, onDelete, onSelect, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more devices before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -210,17 +197,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more devices after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx @@ -70,8 +70,7 @@ export default function ListCategories({ onCreate, onSelect }: Props): VNode { <section class="section is-main-section"> <CardTable devices={result.body.categories} - onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + paginator={{}} onCreate={onCreate} onSelect={(e) => { onSelect(String(e.category_id)); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx @@ -30,6 +30,7 @@ import { ButtonBetterBulma, Loading, LocalNotificationBannerBulma, + PaginationControl, useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -46,6 +47,7 @@ import { ProductWithId, useInstanceProductsFromIds, } from "../../../../hooks/product.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 99; @@ -163,6 +165,7 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { <ProductListSmall onSelect={() => {}} list={category.products} + paginator={{}} /> </FormProvider> </div> @@ -179,9 +182,11 @@ function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { function ProductListSmall({ list, onSelect, + paginator, }: { onSelect: () => void; list: TalerMerchantApi.CategoryProductSummary[]; + paginator: PaginationControl; }): VNode { const { i18n } = useTranslationContext(); const result = useInstanceProductsFromIds(list.map((d) => d.product_id)); @@ -216,7 +221,11 @@ function ProductListSmall({ <div class=" has-pagination"> <div class="table-wrapper has-mobile-cards"> {result.body.length > 0 ? ( - <Table instances={result.body} onSelect={onSelect} /> + <Table + instances={result.body} + onSelect={onSelect} + paginator={paginator} + /> ) : ( <EmptyTable /> )} @@ -230,15 +239,9 @@ function ProductListSmall({ interface TableProps { instances: ProductWithId[]; onSelect: (id: Entity) => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } -function Table({ - instances, - onSelect, - onLoadMoreAfter, - onLoadMoreBefore, -}: TableProps): VNode { +function Table({ instances, onSelect, paginator }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <Fragment> @@ -286,17 +289,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more products after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx @@ -27,6 +27,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, SafeHandlerTemplate, useLocalNotificationBetter, useTranslationContext, @@ -35,6 +36,7 @@ import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 102; @@ -44,17 +46,10 @@ interface Props { devices: Entity[]; // onSelect: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } -export function CardTable({ - devices, - onCreate, - // onSelect, - onLoadMoreAfter, - onLoadMoreBefore, -}: Props): VNode { +export function CardTable({ devices, onCreate, paginator }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); const { i18n } = useTranslationContext(); @@ -106,11 +101,9 @@ export function CardTable({ <Table instances={devices} onDelete={remove} - // onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -128,31 +121,14 @@ interface TableProps { onDelete: SafeHandlerTemplate<[id: string], unknown>; // onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } -function Table({ - instances, - onLoadMoreAfter, - onDelete, - // onSelect, - onLoadMoreBefore, -}: TableProps): VNode { +function Table({ instances, onDelete, paginator }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more groups before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -201,17 +177,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more groups after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx @@ -63,8 +63,7 @@ export default function ListProductGroups({ onCreate }: Props): VNode { <section class="section is-main-section"> <CardTable devices={result.body.groups} - onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + paginator={{}} onCreate={onCreate} /> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -20,7 +20,7 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { PaginationControl, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { WithId } from "../../../../declaration.js"; import { OrderListSection } from "./index.js"; @@ -34,8 +34,7 @@ export interface ListPageProps { section: OrderListSection; orders: (TalerMerchantApi.OrderHistoryEntry & WithId)[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; onSelectOrder: (o: TalerMerchantApi.OrderHistoryEntry & WithId) => void; onRefundOrder: (o: TalerMerchantApi.OrderHistoryEntry & WithId) => void; @@ -43,14 +42,13 @@ export interface ListPageProps { } export function ListPage({ - onLoadMoreAfter, - onLoadMoreBefore, orders, onSelectOrder, onRefundOrder, onChangeSection, section, onCreate, + paginator, }: ListPageProps): VNode { const { i18n } = useTranslationContext(); @@ -151,8 +149,7 @@ export function ListPage({ section={section} onSelect={onSelectOrder} onRefund={onRefundOrder} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -33,6 +33,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, RenderAmount, RenderAmountBulma, useLocalNotificationBetter, @@ -72,8 +73,7 @@ interface Props { onCreate: () => void; section?: OrderListSection; onSelect: (order: Entity) => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ @@ -82,8 +82,7 @@ export function CardTable({ onCreate, onRefund, onSelect, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -127,9 +126,8 @@ export function CardTable({ onRefund={onRefund} section={section} rowSelection={rowSelection} + paginator={paginator} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} /> ) : ( <EmptyTable /> @@ -146,18 +144,64 @@ interface TableProps { onRefund: (id: Entity) => void; onSelect: (id: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; section?: OrderListSection; } +export function PaginationRow({ paginator }: { paginator: PaginationControl }) { + const { i18n } = useTranslationContext(); + return ( + <div + style={{ + display: "flex", + justifyContent: "space-between", + margin: 8, + }} + > + <div style={{ display: "flex", gap: 8 }}> + <Tooltip text={i18n.str`Load more orders after the last one`}> + <button + type="button" + class="button" + onClick={paginator.loadFirst} + disabled={!paginator.loadFirst} + > + <i class="icon mdi mdi-arrow-collapse-left" /> + </button> + </Tooltip> + <Tooltip text={i18n.str`Load more orders after the last one`}> + <button + type="button" + class="button" + onClick={paginator.loadPrev} + disabled={!paginator.loadPrev} + > + <i class="icon mdi mdi-arrow-left" /> + </button> + </Tooltip> + </div> + <div> + <Tooltip text={i18n.str`Load more orders after the last one`}> + <button + type="button" + class="button" + onClick={paginator.loadNext} + disabled={!paginator.loadNext} + > + <i class="icon mdi mdi-arrow-right" /> + </button> + </Tooltip> + </div> + </div> + ); +} + function Table({ instances, section, onSelect, onRefund, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); const [preferences] = usePreference(); @@ -187,15 +231,7 @@ function Table({ <LocalNotificationBannerBulma notification={notification} /> <div class=""> - {onLoadMoreBefore && ( - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - )} + <PaginationRow paginator={paginator} /> <table class="table is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -293,17 +329,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more orders after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -104,20 +104,15 @@ export default function OrderList({ }: Props): VNode { const [filter, setFilter] = useState<{ date?: AbsoluteTime; - position?: string; }>({}); const [orderToBeRefunded, setOrderToBeRefunded] = useState< TalerMerchantApi.OrderHistoryEntry | undefined >(undefined); - const setNewDate = (date?: AbsoluteTime): void => - setFilter((prev) => ({ ...prev, date })); + const setNewDate = (date?: AbsoluteTime): void => setFilter({ date }); - const result = useInstanceOrders( - { ...sectionToFilter(section), ...filter }, - (d) => setFilter({ ...filter, position: d }), - ); + const result = useInstanceOrders({ ...sectionToFilter(section), ...filter }); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { state: session, lib } = useSessionContext(); @@ -251,8 +246,7 @@ export default function OrderList({ /> <ListPage orders={result.body.map((o) => ({ ...o, id: o.order_id }))} - onLoadMoreBefore={result.loadFirst} - onLoadMoreAfter={result.loadNext} + paginator={result} onSelectOrder={(order) => onSelect(order.id)} onRefundOrder={(value) => setOrderToBeRefunded(value)} section={section} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx @@ -27,6 +27,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, SafeHandlerTemplate, useLocalNotificationBetter, useTranslationContext, @@ -35,6 +36,7 @@ import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { useSessionContext } from "../../../../context/session.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 51; @@ -44,16 +46,14 @@ interface Props { devices: Entity[]; onSelect: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ devices, onCreate, onSelect, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -109,8 +109,7 @@ export function CardTable({ onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -128,31 +127,19 @@ interface TableProps { onDelete: SafeHandlerTemplate<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ instances, - onLoadMoreAfter, onDelete, onSelect, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more devices before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -201,17 +188,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more devices after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx @@ -25,7 +25,10 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + LocalNotificationBannerBulma, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; @@ -67,12 +70,10 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return ( <Fragment> - <section class="section is-main-section"> <CardTable devices={result.body.otp_devices} - onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + paginator={{}} onCreate={onCreate} onSelect={(e) => { onSelect(e.otp_device_id); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx @@ -27,6 +27,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, SafeHandlerTemplate, useLocalNotificationBetter, useTranslationContext, @@ -35,6 +36,7 @@ import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 101; @@ -44,16 +46,14 @@ interface Props { pots: Entity[]; onSelect: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ pots, onCreate, onSelect, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -109,8 +109,7 @@ export function CardTable({ onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -128,31 +127,19 @@ interface TableProps { onDelete: SafeHandlerTemplate<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ instances, - onLoadMoreAfter, onDelete, onSelect, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more pots before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -201,17 +188,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more pots after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx @@ -63,8 +63,7 @@ export default function ListMoneyPots({ onCreate, onSelect }: Props): VNode { <section class="section is-main-section"> <CardTable pots={result.body.pots} - onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + paginator={{}} onCreate={onCreate} onSelect={(e) => { onSelect(String(e.pot_serial)); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -29,6 +29,7 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + PaginationControl, RenderAmountBulma, useLocalNotificationBetter, useTranslationContext, @@ -53,6 +54,7 @@ import { usePreference, } from "../../../../hooks/preference.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 56; @@ -64,8 +66,7 @@ interface Props { onSelect: (product: Entity) => void; onCreate?: () => void; selected?: boolean; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ @@ -73,8 +74,7 @@ export function CardTable({ onCreate, onSelect, onDelete, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string | undefined>( undefined, @@ -110,8 +110,7 @@ export function CardTable({ instances={instances} onSelect={onSelect} onDelete={onDelete} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> @@ -130,8 +129,7 @@ interface TableProps { onSelect: (id: Entity) => void; onDelete: ((id: Entity) => void) | undefined; rowSelectionHandler: StateUpdater<string | undefined>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ @@ -140,8 +138,7 @@ function Table({ instances, onSelect, onDelete, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); const [preference] = usePreference(); @@ -166,15 +163,7 @@ function Table({ }; return ( <div class=""> - {onLoadMoreBefore && ( - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - )} + <PaginationRow paginator={paginator} /> <table class="table is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -385,17 +374,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more products after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -59,7 +59,8 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { i18n } = useTranslationContext(); - const remove = safeFunctionHandler(i18n.str`delete product`, + const remove = safeFunctionHandler( + i18n.str`delete product`, lib.instance.deleteProduct.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.id], ); @@ -102,7 +103,6 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { <LocalNotificationBannerBulma notification={notification} /> <div style={{ marginBottom: 10 }}> - <JumpToElementById onSelect={onSelect} description={i18n.str`Jump to product with the given product ID`} @@ -112,8 +112,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { <CardTable instances={result.body} - onLoadMoreBefore={result.loadFirst} - onLoadMoreAfter={result.loadNext} + paginator={result} onCreate={onCreate} onSelect={(product) => onSelect(product.id)} onDelete={(prod: TalerMerchantApi.ProductDetailResponse & WithId) => diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx @@ -28,6 +28,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, SafeHandlerTemplate, useLocalNotificationBetter, useTranslationContext, @@ -37,6 +38,7 @@ import { StateUpdater, useState } from "preact/hooks"; import { durationToString } from "../../../../components/form/InputDurationSelector.js"; import { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 100; @@ -46,16 +48,14 @@ interface Props { reports: Entity[]; onSelect: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ reports, onCreate, onSelect, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -111,8 +111,7 @@ export function CardTable({ onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -130,31 +129,19 @@ interface TableProps { onDelete: SafeHandlerTemplate<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ instances, - onLoadMoreAfter, onDelete, onSelect, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more devices before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -206,17 +193,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more devices after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx @@ -22,7 +22,7 @@ import { HttpStatusCode, TalerError, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -37,7 +37,10 @@ interface Props { onSelect: (id: string) => void; } -export default function ListScheduledReport({ onCreate, onSelect }: Props): VNode { +export default function ListScheduledReport({ + onCreate, + onSelect, +}: Props): VNode { const result = useInstanceScheduledReports(); if (!result) return <Loading />; @@ -60,12 +63,10 @@ export default function ListScheduledReport({ onCreate, onSelect }: Props): VNod return ( <Fragment> - <section class="section is-main-section"> <CardTable reports={result.body.reports} - onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + paginator={{}} onCreate={onCreate} onSelect={(e) => { onSelect(String(e.report_serial)); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/Table.tsx @@ -19,7 +19,10 @@ * @author Martin Schanzenbach */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + PaginationControl, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { StatSlug } from "./index.js"; @@ -33,8 +36,7 @@ interface Props { amountStatSlugs: Entity[]; onSelectAmountStat: (e: string) => void; onSelectCounterStat: (e: string) => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -20,10 +20,14 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + PaginationControl, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 63; @@ -36,8 +40,7 @@ interface Props { onNewOrder: (e: Entity) => void; onQR: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ @@ -47,8 +50,7 @@ export function CardTable({ onSelect, onQR, onNewOrder, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -86,8 +88,7 @@ export function CardTable({ onQR={onQR} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -106,33 +107,21 @@ interface TableProps { onQR: (e: Entity) => void; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ instances, - onLoadMoreAfter, onDelete, onNewOrder, onQR, onSelect, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more templates before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -201,17 +190,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more templates after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -121,8 +121,7 @@ export default function ListTemplates({ ...o, id: String(o.template_id), }))} - onLoadMoreBefore={undefined} - onLoadMoreAfter={undefined} + paginator={{}} onCreate={onCreate} onSelect={(e) => { onSelect(e.template_id); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -41,22 +41,18 @@ export function ListPage({ transfers, incomings, onDetails }: Props): VNode { ...o, id: String(o.wtid), }))} - onLoadMoreBefore={incomings.loadFirst} - onLoadMoreAfter={incomings.loadNext} + paginator={incomings} onDetails={onDetails} /> )} - {/* // ) : ( */} <CardTableVerified transfers={transfers.body.map((o) => ({ ...o, id: String(o.wtid), }))} - onLoadMoreBefore={transfers.loadFirst} - onLoadMoreAfter={transfers.loadNext} + paginator={transfers} onDetails={onDetails} /> - {/* // )} */} </section> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -29,6 +29,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, RenderAmountBulma, useLocalNotificationBetter, useTranslationContext, @@ -43,20 +44,19 @@ import { } from "../../../../hooks/preference.js"; import { Tooltip } from "../../../../components/Tooltip.js"; import { revalidateInstanceIncomingTransfers } from "../../../../hooks/transfer.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 72; interface TablePropsIncoming { transfers: (TalerMerchantApi.ExpectedTransferEntry & WithId)[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; onDetails: (d: number) => void; } export function CardTableIncoming({ transfers, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, onDetails, }: TablePropsIncoming): VNode { const { i18n } = useTranslationContext(); @@ -107,19 +107,7 @@ export function CardTableIncoming({ <div class="table-wrapper has-mobile-cards"> {transfers.length > 0 ? ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip - text={i18n.str`Load more wire transfers preceding the first one`} - > - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable"> <thead> <tr> @@ -210,19 +198,7 @@ export function CardTableIncoming({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip - text={i18n.str`Load more transfers after the last one`} - > - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ) : ( <EmptyTable /> @@ -237,15 +213,13 @@ export function CardTableIncoming({ interface TablePropsVerified { transfers: (TalerMerchantApi.TransferDetails & WithId)[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; onDetails: (d: number) => void; } export function CardTableVerified({ transfers, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, onDetails, }: TablePropsVerified): VNode { const { i18n } = useTranslationContext(); @@ -266,19 +240,7 @@ export function CardTableVerified({ <div class="table-wrapper has-mobile-cards"> {transfers.length > 0 ? ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip - text={i18n.str`Load more wire transfers preceding the first one`} - > - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable"> <thead> <tr> @@ -342,19 +304,7 @@ export function CardTableVerified({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip - text={i18n.str`Load more transfers after the last one`} - > - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ) : ( <EmptyTable /> @@ -376,9 +326,7 @@ function EmptyTable(): VNode { </span> </p> <p> - <i18n.Translate> - There are no transfers to list yet - </i18n.Translate> + <i18n.Translate>There are no transfers to list yet</i18n.Translate> </p> </div> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -91,7 +91,6 @@ function ListTransferWithBank({ onTransferDetails: (id: number) => void; bankAccounts: BankAccountEntry[]; }): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); const accounts = bankAccounts.map((b) => b.payto_uri); const [form, setForm] = useState<Form>({ payto_uri: accounts.length > 0 ? accounts[0] : "", @@ -101,12 +100,10 @@ function ListTransferWithBank({ { const result = useInstanceIncomingTransfers( { - position, payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, verified: form.verified, confirmed: false, }, - (id) => setPosition(id), ); if (!result) return <Loading />; @@ -132,12 +129,10 @@ function ListTransferWithBank({ { const result = useInstanceConfirmedTransfers( { - position, payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, // expected: form.expected, verified: form.verified, }, - (id) => setPosition(id), ); if (!result) return <Loading />; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx @@ -20,14 +20,16 @@ */ import { h, VNode } from "preact"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + PaginationControl, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { CardTable } from "./Table.js"; import { TalerMerchantApi } from "@gnu-taler/taler-util"; export interface Props { webhooks: TalerMerchantApi.WebhookEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; onCreate: () => void; onSelect: (e: TalerMerchantApi.WebhookEntry) => void; } @@ -36,8 +38,7 @@ export function ListPage({ webhooks, onCreate, onSelect, - onLoadMoreBefore, - onLoadMoreAfter, + paginator, }: Props): VNode { return ( <section class="section is-main-section"> @@ -48,8 +49,7 @@ export function ListPage({ }))} onCreate={onCreate} onSelect={onSelect} - onLoadMoreBefore={onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} + paginator={paginator} /> </section> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -28,6 +28,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + PaginationControl, SafeHandlerTemplate, useLocalNotificationBetter, useTranslationContext, @@ -36,6 +37,7 @@ import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { useSessionContext } from "../../../../context/session.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 77; @@ -45,16 +47,14 @@ interface Props { webhooks: Entity[]; onSelect: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } export function CardTable({ webhooks, onCreate, onSelect, - onLoadMoreAfter, - onLoadMoreBefore, + paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -115,8 +115,7 @@ export function CardTable({ onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} + paginator={paginator} /> ) : ( <EmptyTable /> @@ -134,31 +133,19 @@ interface TableProps { deleteWebhook: SafeHandlerTemplate<[id: string], any>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + paginator: PaginationControl; } function Table({ instances, - onLoadMoreAfter, deleteWebhook, onSelect, - onLoadMoreBefore, + paginator, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class=""> - {onLoadMoreBefore && ( - <Tooltip text={i18n.str`Load more webhooks before the first one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>Load first page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -207,17 +194,7 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( - <Tooltip text={i18n.str`Load more webhooks after the last one`}> - <button - type="button" - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>Load next page</i18n.Translate> - </button> - </Tooltip> - )} + <PaginationRow paginator={paginator} /> </div> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx @@ -22,7 +22,7 @@ import { HttpStatusCode, TalerError, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -60,11 +60,9 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { return ( <Fragment> - <ListPage webhooks={result.body.webhooks} - onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} - onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + paginator={{}} onCreate={onCreate} onSelect={(e) => { onSelect(e.webhook_id); diff --git a/packages/merchant-backoffice-ui/src/utils/constants.ts b/packages/merchant-backoffice-ui/src/utils/constants.ts @@ -35,7 +35,8 @@ export const CROCKFORD_BASE32_REGEX = export const URL_REGEX = /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/; -export const PAGINATED_LIST_SIZE = 20; + +const PAGINATED_LIST_SIZE = 4; // when doing paginated request, ask for one more // and use it to know if there are more to request export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1; diff --git a/packages/web-util/src/utils/buildPaginatedResult.ts b/packages/web-util/src/utils/buildPaginatedResult.ts @@ -1,12 +1,15 @@ -import { OperationOk } from "@gnu-taler/taler-util"; +import { assertUnreachable, OperationOk } from "@gnu-taler/taler-util"; +import { useState } from "preact/hooks"; -export type PaginatedResult<T> = OperationOk<T> & { +export type PaginationControl = { loadNext?(): void; + loadPrev?(): void; loadFirst?(): void; -}; +} +export type PaginatedResult<T> = OperationOk<T> & PaginationControl; /** - * + * * @param data the result of the requested list * @param offset offset id or index * @param setOffset function to be call on loadNext or loadFirst to specify the new offset @@ -14,36 +17,113 @@ export type PaginatedResult<T> = OperationOk<T> & { * @param PAGINATED_LIST_REQUEST the limit of the request, the UI is expted to show N -1 elements * @returns an OperationOk with two function. If the function is missing is because the offset is on the limit */ -export function buildPaginatedResult<R, OffId>( +export function buildPaginatedResult<R>( data: R[], - offset: OffId | undefined, - setOffset: (o: OffId | undefined) => void, - getId: (r: R) => OffId, - PAGINATED_LIST_REQUEST: number + offset: ListPointer, + setOffset: (o: R | undefined, direction: "asc" | "dec") => void, + PAGINATED_LIST_REQUEST: number, ): PaginatedResult<R[]> { - const isLastPage = data.length < PAGINATED_LIST_REQUEST; - const isFirstPage = offset === undefined; - - const result = structuredClone(data); - - if (result.length == PAGINATED_LIST_REQUEST) { - result.pop(); - } + const { + list: body, + isLastPage, + isFirstPage, + } = __compute_for_ui(data, offset, PAGINATED_LIST_REQUEST); return { type: "ok", case: "ok", - body: result, + body, loadNext: isLastPage ? undefined : () => { - if (!result.length) return; - const id = getId(result[result.length - 1]); - setOffset(id); + if (!body.length) return; + const id = body[body.length - 1]; + setOffset(id, "dec"); + }, + loadPrev: isFirstPage + ? undefined + : () => { + if (!body.length) return; + const id = body[0]; + setOffset(id, "asc"); }, loadFirst: isFirstPage ? undefined : () => { - setOffset(undefined); + setOffset(undefined, "dec"); }, }; } + +/** + * based on the pointer and the max result set + * + * return relative pos and list to show + * @param data + * @param offset + * @param max + * @returns + */ +function __compute_for_ui<R>(data: Array<R>, offset: ListPointer, max: number) { + // we ask for N but show N-1 + // the last element on the list is to signal if + // we have more pages after it + const thereIsMore = data.length == max; + const result: R[] = structuredClone(data); + if (thereIsMore) { + result.pop(); + } + // we assume the UI always show on DEC order + // so what the offset.order is telling is + // in which direction the last request was. + switch (offset.order) { + case "asc": { + const isFirstPage = !thereIsMore; + const isLastPage = offset.id === undefined; + const list = result.reverse(); + return { list, isFirstPage, isLastPage }; + } + case "dec": { + const isLastPage = !thereIsMore; + const isFirstPage = offset.id === undefined; + const list = result; + return { list, isFirstPage, isLastPage }; + } + default: { + assertUnreachable(offset.order); + } + } +} + +/** + * Define the entry point of a list when asking for a resultset. + * + * If id is not set then assume the top most entry point. + */ +export type ListPointer = { id?: string; order: "asc" | "dec" }; + +/** + * + * @param getId given a row type T return the rowId + * @returns current pointer and a function to move + */ +export function useListPointer<T>( + getId: (d: T) => string, +): [ListPointer, (p: T | undefined, order: ListPointer["order"]) => void] { + const initial = { + order: "dec" as const, + }; + const [pointer, setPointer] = useState<ListPointer>(initial); + return [ + pointer, + (d, order) => { + if (!d) { + setPointer(initial); + } else { + setPointer({ + order, + id: getId(d), + }); + } + }, + ]; +}