Skip to content

Commit 9a19630

Browse files
committed
feat: implement pagination support in resource list fetching
1 parent 194de77 commit 9a19630

6 files changed

Lines changed: 259 additions & 18 deletions

File tree

packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,19 @@ export interface FetcherResponse<TResource> {
2929
}
3030
}
3131

32-
export async function infiniteFetcher<TResource extends ListableResourceType>({
32+
export async function listFetcher<TResource extends ListableResourceType>({
3333
currentData,
3434
resourceType,
3535
client,
3636
clientType,
3737
query,
38+
mode = "infinite",
39+
pageNumber,
3840
}: {
3941
currentData?: FetcherResponse<Resource<TResource>>
4042
resourceType: TResource
43+
mode?: "infinite" | "pagination"
44+
pageNumber?: number
4145
} & (
4246
| {
4347
client: CommerceLayerClient
@@ -51,7 +55,8 @@ export async function infiniteFetcher<TResource extends ListableResourceType>({
5155
}
5256
)): Promise<FetcherResponse<Resource<TResource>>> {
5357
const currentPage = currentData?.meta.currentPage ?? 0
54-
const pageToFetch = currentPage + 1
58+
const pageToFetch =
59+
mode === "pagination" && pageNumber != null ? pageNumber : currentPage + 1
5560

5661
if (clientType === "metricsClient" && !isValidMetricsResource(resourceType)) {
5762
throw new Error("Metrics client is not available for this resource type")
@@ -75,7 +80,11 @@ export async function infiniteFetcher<TResource extends ListableResourceType>({
7580
// we need the primitive array
7681
// without the sdk added methods ('meta' | 'first' | 'last' | 'get')
7782
const existingList = currentData?.list ?? []
78-
const uniqueList = uniqBy(existingList.concat(listResponse), "id")
83+
// In pagination mode, replace the list instead of accumulating
84+
const uniqueList =
85+
mode === "pagination"
86+
? [...listResponse]
87+
: uniqBy(existingList.concat(listResponse), "id")
7988
const meta = listResponse.meta
8089

8190
return { list: uniqueList, meta }
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type {
2+
CommerceLayerClient,
3+
ListableResourceType,
4+
QueryParamsList,
5+
ResourceFields,
6+
} from "@commercelayer/sdk"
7+
import uniqBy from "lodash-es/uniqBy"
8+
import {
9+
isValidMetricsResource,
10+
type MetricsApiClient,
11+
type MetricsResources,
12+
} from "./metricsApiClient"
13+
14+
type ListResource<TResource extends ListableResourceType> = Awaited<
15+
ReturnType<CommerceLayerClient[TResource]["list"]>
16+
>
17+
18+
export type Resource<TResource extends ListableResourceType> =
19+
ListResource<TResource>[number]
20+
21+
export interface FetcherResponse<TResource> {
22+
list: TResource[]
23+
meta: {
24+
pageCount: number
25+
recordCount: number
26+
currentPage: number
27+
recordsPerPage: number
28+
cursor?: string | null
29+
}
30+
}
31+
32+
export async function listFetcher<TResource extends ListableResourceType>({
33+
currentData,
34+
resourceType,
35+
client,
36+
clientType,
37+
query,
38+
mode = "infinite",
39+
pageNumber,
40+
}: {
41+
currentData?: FetcherResponse<Resource<TResource>>
42+
resourceType: TResource
43+
mode?: "infinite" | "pagination"
44+
pageNumber?: number
45+
} & (
46+
| {
47+
client: CommerceLayerClient
48+
clientType: "coreSdkClient"
49+
query?: Omit<QueryParamsList<ResourceFields[TResource]>, "pageNumber">
50+
}
51+
| {
52+
client: MetricsApiClient
53+
clientType: "metricsClient"
54+
query: Record<string, Record<string, unknown>>
55+
}
56+
)): Promise<FetcherResponse<Resource<TResource>>> {
57+
const currentPage = currentData?.meta.currentPage ?? 0
58+
const pageToFetch =
59+
mode === "pagination" && pageNumber != null ? pageNumber : currentPage + 1
60+
61+
if (clientType === "metricsClient" && !isValidMetricsResource(resourceType)) {
62+
throw new Error("Metrics client is not available for this resource type")
63+
}
64+
65+
const listResponse =
66+
clientType === "metricsClient"
67+
? await client.list(resourceType as MetricsResources, {
68+
...query,
69+
search: {
70+
...query.search,
71+
cursor: currentData?.meta.cursor ?? null,
72+
},
73+
})
74+
: // @ts-expect-error "Expression produces a union type that is too complex to represent"
75+
await client[resourceType].list({
76+
...query,
77+
pageNumber: pageToFetch,
78+
})
79+
80+
// we need the primitive array
81+
// without the sdk added methods ('meta' | 'first' | 'last' | 'get')
82+
const existingList = currentData?.list ?? []
83+
// In pagination mode, replace the list instead of accumulating
84+
const uniqueList =
85+
mode === "pagination"
86+
? [...listResponse]
87+
: uniqBy(existingList.concat(listResponse), "id")
88+
const meta = listResponse.meta
89+
90+
return { list: uniqueList, meta }
91+
}

packages/app-elements/src/ui/resources/useResourceList/metricsApiClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
adaptMetricsOrderToCore,
1313
type MetricsResourceOrder,
1414
} from "./adaptMetricsOrderToCore"
15-
import type { Resource } from "./infiniteFetcher"
15+
import type { Resource } from "./listFetcher"
1616

1717
export type MetricsResources = "orders" | "returns"
1818

packages/app-elements/src/ui/resources/useResourceList/reducer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ListableResourceType } from "@commercelayer/sdk"
2-
import type { FetcherResponse, Resource } from "./infiniteFetcher"
2+
import type { FetcherResponse, Resource } from "./listFetcher"
33

44
interface ResourceListInternalState<TResource extends ListableResourceType> {
55
isLoading: boolean

packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { t } from "#providers/I18NProvider"
2020
import { Button } from "#ui/atoms/Button"
2121
import { Card } from "#ui/atoms/Card"
2222
import { EmptyState } from "#ui/atoms/EmptyState"
23+
import { Pagination } from "#ui/atoms/Pagination"
2324
import { Section, type SectionProps } from "#ui/atoms/Section"
2425
import {
2526
SkeletonTemplate,
@@ -30,7 +31,7 @@ import { Table, Th, Tr } from "#ui/atoms/Table"
3031
import type { ThProps } from "#ui/atoms/Table/Th"
3132
import { Text } from "#ui/atoms/Text"
3233
import { InputFeedback } from "#ui/forms/InputFeedback"
33-
import { infiniteFetcher, type Resource } from "./infiniteFetcher"
34+
import { listFetcher, type Resource } from "./listFetcher"
3435
import { useMetricsSdkProvider } from "./metricsApiClient"
3536
import { initialState, reducer } from "./reducer"
3637
import { computeTitleWithTotalCount } from "./utils"
@@ -114,21 +115,27 @@ export interface UseResourceListConfig<TResource extends ListableResourceType> {
114115
}
115116
filter: Record<string, unknown>
116117
}
118+
/**
119+
* Pagination type: 'infinite' for infinite scrolling (default), 'pagination' for classic prev/next pagination.
120+
* Note: 'pagination' mode is only supported for Core API (not Metrics API).
121+
*/
122+
paginationType?: "infinite" | "pagination"
117123
}
118124

119125
/**
120-
* Renders a list of resources of a given type with infinite scrolling.
126+
* Renders a list of resources of a given type with infinite scrolling or classic pagination.
121127
* It's possible to specify a query to filter the list and either
122128
* a React component (`ItemTemplate`) to be used as item template for the list or a function as `children` to render a custom element.
123129
*/
124130
export function useResourceList<TResource extends ListableResourceType>({
125131
type,
126132
query,
127133
metricsQuery,
134+
paginationType = "infinite",
128135
}: UseResourceListConfig<TResource>): {
129-
/** The component that renders the list with infinite scrolling functionality */
136+
/** The component that renders the list with infinite scrolling or pagination functionality */
130137
ResourceList: FC<ResourceListProps<TResource>>
131-
/** The raw array of fetched resources, which grows each time a new page is fetched */
138+
/** The raw array of fetched resources, which grows each time a new page is fetched (infinite mode) or shows current page only (pagination mode) */
132139
list?: Array<Resource<TResource>>
133140
/** Metadata related to pagination, as returned by the SDK */
134141
meta?: ListMeta
@@ -156,27 +163,40 @@ export function useResourceList<TResource extends ListableResourceType>({
156163
reducer,
157164
initialState,
158165
)
166+
const [currentPage, setCurrentPage] = React.useState(1)
167+
168+
// Validate that pagination mode is not used with metrics API
169+
if (paginationType === "pagination" && metricsQuery != null) {
170+
throw new Error(
171+
"Pagination mode is not supported with Metrics API. Please use infinite scrolling (default) or switch to Core API.",
172+
)
173+
}
159174

160175
const isQueryChanged = useIsChanged({
161176
value: query,
162177
onChange: () => {
178+
setCurrentPage(1)
163179
dispatch({ type: "reset" })
164-
void fetchMore({ query })
180+
void fetchMore({ query, pageNumber: 1 })
165181
},
166182
})
167183

168184
const fetchMore = useCallback(
169185
async ({
170186
query,
187+
pageNumber,
171188
}: {
172189
query?: Omit<QueryParamsList<ResourceFields[TResource]>, "pageNumber">
190+
pageNumber?: number
173191
}): Promise<void> => {
174192
dispatch({ type: "prepare" })
175193
try {
176-
const listResponse = await infiniteFetcher({
194+
const listResponse = await listFetcher({
177195
// when is new query, we don't want to pass existing data
178196
currentData: isQueryChanged ? undefined : data,
179197
resourceType: type,
198+
mode: paginationType,
199+
pageNumber,
180200
...(metricsQuery != null
181201
? {
182202
clientType: "metricsClient",
@@ -194,12 +214,15 @@ export function useResourceList<TResource extends ListableResourceType>({
194214
dispatch({ type: "error", payload: parseApiErrorMessage(err) })
195215
}
196216
},
197-
[sdkClient, data, isQueryChanged],
217+
[sdkClient, data, isQueryChanged, paginationType, metricsQuery, type],
198218
)
199219

200220
useEffect(
201221
function initialFetch() {
202-
void fetchMore({ query })
222+
void fetchMore({
223+
query,
224+
pageNumber: paginationType === "pagination" ? 1 : undefined,
225+
})
203226
},
204227
[sdkClient],
205228
)
@@ -221,9 +244,21 @@ export function useResourceList<TResource extends ListableResourceType>({
221244
}, [])
222245

223246
const refresh = useCallback(() => {
247+
setCurrentPage(1)
224248
dispatch({ type: "reset" })
225-
void fetchMore({ query })
226-
}, [])
249+
void fetchMore({
250+
query,
251+
pageNumber: paginationType === "pagination" ? 1 : undefined,
252+
})
253+
}, [query, paginationType])
254+
255+
const handlePageChange = useCallback(
256+
(newPage: number) => {
257+
setCurrentPage(newPage)
258+
void fetchMore({ query, pageNumber: newPage })
259+
},
260+
[query, fetchMore],
261+
)
227262

228263
const ResourceList = useCallback<FC<ResourceListProps<TResource>>>(
229264
({
@@ -311,7 +346,13 @@ export function useResourceList<TResource extends ListableResourceType>({
311346
<ErrorLine
312347
message={error.message}
313348
onRetry={() => {
314-
void fetchMore({ query })
349+
void fetchMore({
350+
query,
351+
pageNumber:
352+
paginationType === "pagination"
353+
? currentPage
354+
: undefined,
355+
})
315356
}}
316357
/>
317358
) : isLoading ? (
@@ -321,7 +362,18 @@ export function useResourceList<TResource extends ListableResourceType>({
321362
// biome-ignore lint/suspicious/noArrayIndexKey: Using index as key is acceptable here since items are static
322363
<ItemTemplate isLoading delayMs={0} key={idx} />
323364
))
324-
) : (
365+
) : paginationType === "pagination" &&
366+
data != null &&
367+
data.meta.pageCount > 1 ? (
368+
<Spacer top="6">
369+
<Pagination
370+
currentPage={currentPage}
371+
pageCount={data.meta.pageCount}
372+
isDisabled={isLoading}
373+
onChangePageRequest={handlePageChange}
374+
/>
375+
</Spacer>
376+
) : paginationType === "infinite" ? (
325377
<VisibilityTrigger
326378
enabled={hasMorePages}
327379
callback={(entry) => {
@@ -330,7 +382,7 @@ export function useResourceList<TResource extends ListableResourceType>({
330382
}
331383
}}
332384
/>
333-
)
385+
) : null
334386
}
335387
>
336388
{data?.list.map((resource) => {
@@ -358,6 +410,11 @@ export function useResourceList<TResource extends ListableResourceType>({
358410
isLoading,
359411
isFirstLoading,
360412
error,
413+
paginationType,
414+
currentPage,
415+
handlePageChange,
416+
query,
417+
fetchMore,
361418
],
362419
)
363420

0 commit comments

Comments
 (0)