From 9a196305cf0ec77f54d7f9c310bf13d3a52f555a Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:02:56 +0100 Subject: [PATCH 1/7] feat: implement pagination support in resource list fetching --- .../useResourceList/infiniteFetcher.ts | 15 ++- .../resources/useResourceList/listFetcher.ts | 91 +++++++++++++++++++ .../useResourceList/metricsApiClient.ts | 2 +- .../ui/resources/useResourceList/reducer.ts | 2 +- .../useResourceList/useResourceList.tsx | 83 ++++++++++++++--- .../resources/useResourceList.stories.tsx | 84 +++++++++++++++++ 6 files changed, 259 insertions(+), 18 deletions(-) create mode 100644 packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts diff --git a/packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts b/packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts index 9db99ed25..6cd5530c1 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts +++ b/packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts @@ -29,15 +29,19 @@ export interface FetcherResponse { } } -export async function infiniteFetcher({ +export async function listFetcher({ currentData, resourceType, client, clientType, query, + mode = "infinite", + pageNumber, }: { currentData?: FetcherResponse> resourceType: TResource + mode?: "infinite" | "pagination" + pageNumber?: number } & ( | { client: CommerceLayerClient @@ -51,7 +55,8 @@ export async function infiniteFetcher({ } )): Promise>> { const currentPage = currentData?.meta.currentPage ?? 0 - const pageToFetch = currentPage + 1 + const pageToFetch = + mode === "pagination" && pageNumber != null ? pageNumber : currentPage + 1 if (clientType === "metricsClient" && !isValidMetricsResource(resourceType)) { throw new Error("Metrics client is not available for this resource type") @@ -75,7 +80,11 @@ export async function infiniteFetcher({ // we need the primitive array // without the sdk added methods ('meta' | 'first' | 'last' | 'get') const existingList = currentData?.list ?? [] - const uniqueList = uniqBy(existingList.concat(listResponse), "id") + // In pagination mode, replace the list instead of accumulating + const uniqueList = + mode === "pagination" + ? [...listResponse] + : uniqBy(existingList.concat(listResponse), "id") const meta = listResponse.meta return { list: uniqueList, meta } diff --git a/packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts b/packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts new file mode 100644 index 000000000..6cd5530c1 --- /dev/null +++ b/packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts @@ -0,0 +1,91 @@ +import type { + CommerceLayerClient, + ListableResourceType, + QueryParamsList, + ResourceFields, +} from "@commercelayer/sdk" +import uniqBy from "lodash-es/uniqBy" +import { + isValidMetricsResource, + type MetricsApiClient, + type MetricsResources, +} from "./metricsApiClient" + +type ListResource = Awaited< + ReturnType +> + +export type Resource = + ListResource[number] + +export interface FetcherResponse { + list: TResource[] + meta: { + pageCount: number + recordCount: number + currentPage: number + recordsPerPage: number + cursor?: string | null + } +} + +export async function listFetcher({ + currentData, + resourceType, + client, + clientType, + query, + mode = "infinite", + pageNumber, +}: { + currentData?: FetcherResponse> + resourceType: TResource + mode?: "infinite" | "pagination" + pageNumber?: number +} & ( + | { + client: CommerceLayerClient + clientType: "coreSdkClient" + query?: Omit, "pageNumber"> + } + | { + client: MetricsApiClient + clientType: "metricsClient" + query: Record> + } +)): Promise>> { + const currentPage = currentData?.meta.currentPage ?? 0 + const pageToFetch = + mode === "pagination" && pageNumber != null ? pageNumber : currentPage + 1 + + if (clientType === "metricsClient" && !isValidMetricsResource(resourceType)) { + throw new Error("Metrics client is not available for this resource type") + } + + const listResponse = + clientType === "metricsClient" + ? await client.list(resourceType as MetricsResources, { + ...query, + search: { + ...query.search, + cursor: currentData?.meta.cursor ?? null, + }, + }) + : // @ts-expect-error "Expression produces a union type that is too complex to represent" + await client[resourceType].list({ + ...query, + pageNumber: pageToFetch, + }) + + // we need the primitive array + // without the sdk added methods ('meta' | 'first' | 'last' | 'get') + const existingList = currentData?.list ?? [] + // In pagination mode, replace the list instead of accumulating + const uniqueList = + mode === "pagination" + ? [...listResponse] + : uniqBy(existingList.concat(listResponse), "id") + const meta = listResponse.meta + + return { list: uniqueList, meta } +} diff --git a/packages/app-elements/src/ui/resources/useResourceList/metricsApiClient.ts b/packages/app-elements/src/ui/resources/useResourceList/metricsApiClient.ts index 9cf3e33c9..6f113a3ae 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/metricsApiClient.ts +++ b/packages/app-elements/src/ui/resources/useResourceList/metricsApiClient.ts @@ -12,7 +12,7 @@ import { adaptMetricsOrderToCore, type MetricsResourceOrder, } from "./adaptMetricsOrderToCore" -import type { Resource } from "./infiniteFetcher" +import type { Resource } from "./listFetcher" export type MetricsResources = "orders" | "returns" diff --git a/packages/app-elements/src/ui/resources/useResourceList/reducer.ts b/packages/app-elements/src/ui/resources/useResourceList/reducer.ts index 935837699..56218b1b6 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/reducer.ts +++ b/packages/app-elements/src/ui/resources/useResourceList/reducer.ts @@ -1,5 +1,5 @@ import type { ListableResourceType } from "@commercelayer/sdk" -import type { FetcherResponse, Resource } from "./infiniteFetcher" +import type { FetcherResponse, Resource } from "./listFetcher" interface ResourceListInternalState { isLoading: boolean diff --git a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx index 2c810eafa..10bc57a3a 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx @@ -20,6 +20,7 @@ import { t } from "#providers/I18NProvider" import { Button } from "#ui/atoms/Button" import { Card } from "#ui/atoms/Card" import { EmptyState } from "#ui/atoms/EmptyState" +import { Pagination } from "#ui/atoms/Pagination" import { Section, type SectionProps } from "#ui/atoms/Section" import { SkeletonTemplate, @@ -30,7 +31,7 @@ import { Table, Th, Tr } from "#ui/atoms/Table" import type { ThProps } from "#ui/atoms/Table/Th" import { Text } from "#ui/atoms/Text" import { InputFeedback } from "#ui/forms/InputFeedback" -import { infiniteFetcher, type Resource } from "./infiniteFetcher" +import { listFetcher, type Resource } from "./listFetcher" import { useMetricsSdkProvider } from "./metricsApiClient" import { initialState, reducer } from "./reducer" import { computeTitleWithTotalCount } from "./utils" @@ -114,10 +115,15 @@ export interface UseResourceListConfig { } filter: Record } + /** + * Pagination type: 'infinite' for infinite scrolling (default), 'pagination' for classic prev/next pagination. + * Note: 'pagination' mode is only supported for Core API (not Metrics API). + */ + paginationType?: "infinite" | "pagination" } /** - * Renders a list of resources of a given type with infinite scrolling. + * Renders a list of resources of a given type with infinite scrolling or classic pagination. * It's possible to specify a query to filter the list and either * a React component (`ItemTemplate`) to be used as item template for the list or a function as `children` to render a custom element. */ @@ -125,10 +131,11 @@ export function useResourceList({ type, query, metricsQuery, + paginationType = "infinite", }: UseResourceListConfig): { - /** The component that renders the list with infinite scrolling functionality */ + /** The component that renders the list with infinite scrolling or pagination functionality */ ResourceList: FC> - /** The raw array of fetched resources, which grows each time a new page is fetched */ + /** The raw array of fetched resources, which grows each time a new page is fetched (infinite mode) or shows current page only (pagination mode) */ list?: Array> /** Metadata related to pagination, as returned by the SDK */ meta?: ListMeta @@ -156,27 +163,40 @@ export function useResourceList({ reducer, initialState, ) + const [currentPage, setCurrentPage] = React.useState(1) + + // Validate that pagination mode is not used with metrics API + if (paginationType === "pagination" && metricsQuery != null) { + throw new Error( + "Pagination mode is not supported with Metrics API. Please use infinite scrolling (default) or switch to Core API.", + ) + } const isQueryChanged = useIsChanged({ value: query, onChange: () => { + setCurrentPage(1) dispatch({ type: "reset" }) - void fetchMore({ query }) + void fetchMore({ query, pageNumber: 1 }) }, }) const fetchMore = useCallback( async ({ query, + pageNumber, }: { query?: Omit, "pageNumber"> + pageNumber?: number }): Promise => { dispatch({ type: "prepare" }) try { - const listResponse = await infiniteFetcher({ + const listResponse = await listFetcher({ // when is new query, we don't want to pass existing data currentData: isQueryChanged ? undefined : data, resourceType: type, + mode: paginationType, + pageNumber, ...(metricsQuery != null ? { clientType: "metricsClient", @@ -194,12 +214,15 @@ export function useResourceList({ dispatch({ type: "error", payload: parseApiErrorMessage(err) }) } }, - [sdkClient, data, isQueryChanged], + [sdkClient, data, isQueryChanged, paginationType, metricsQuery, type], ) useEffect( function initialFetch() { - void fetchMore({ query }) + void fetchMore({ + query, + pageNumber: paginationType === "pagination" ? 1 : undefined, + }) }, [sdkClient], ) @@ -221,9 +244,21 @@ export function useResourceList({ }, []) const refresh = useCallback(() => { + setCurrentPage(1) dispatch({ type: "reset" }) - void fetchMore({ query }) - }, []) + void fetchMore({ + query, + pageNumber: paginationType === "pagination" ? 1 : undefined, + }) + }, [query, paginationType]) + + const handlePageChange = useCallback( + (newPage: number) => { + setCurrentPage(newPage) + void fetchMore({ query, pageNumber: newPage }) + }, + [query, fetchMore], + ) const ResourceList = useCallback>>( ({ @@ -311,7 +346,13 @@ export function useResourceList({ { - void fetchMore({ query }) + void fetchMore({ + query, + pageNumber: + paginationType === "pagination" + ? currentPage + : undefined, + }) }} /> ) : isLoading ? ( @@ -321,7 +362,18 @@ export function useResourceList({ // biome-ignore lint/suspicious/noArrayIndexKey: Using index as key is acceptable here since items are static )) - ) : ( + ) : paginationType === "pagination" && + data != null && + data.meta.pageCount > 1 ? ( + + + + ) : paginationType === "infinite" ? ( { @@ -330,7 +382,7 @@ export function useResourceList({ } }} /> - ) + ) : null } > {data?.list.map((resource) => { @@ -358,6 +410,11 @@ export function useResourceList({ isLoading, isFirstLoading, error, + paginationType, + currentPage, + handlePageChange, + query, + fetchMore, ], ) diff --git a/packages/docs/src/stories/resources/useResourceList.stories.tsx b/packages/docs/src/stories/resources/useResourceList.stories.tsx index ce33a1f46..8787a63a7 100644 --- a/packages/docs/src/stories/resources/useResourceList.stories.tsx +++ b/packages/docs/src/stories/resources/useResourceList.stories.tsx @@ -214,3 +214,87 @@ WithInfiniteScrolling.parameters = { }, }, } + +/** + * You can use classic prev/next pagination instead of infinite scrolling by setting `paginationType: "pagination"`. + * This mode is only supported for Core API (not Metrics API). + */ +export const WithPagination: StoryFn = () => { + const { ResourceList } = useResourceList({ + type: "orders", + query: { + pageSize: 10, + }, + paginationType: "pagination", + }) + + return ( + Empty} + actionButton={} + ItemTemplate={({ resource = mockedOrder, isLoading }) => { + return ( + + + + ) + }} + /> + ) +} +WithPagination.parameters = { + docs: { + canvas: { + sourceState: "none", + }, + }, +} + +/** + * Pagination mode also works with table variant. + */ +export const WithPaginationAsTable: StoryFn = () => { + const { ResourceList } = useResourceList({ + type: "orders", + query: { + pageSize: 10, + }, + paginationType: "pagination", + }) + + return ( + + Order + + } + ItemTemplate={({ resource = mockedOrder, isLoading }) => { + return ( + + #{resource.number} + {resource.market?.name} + + {resource.formatted_total_amount} + + + ) + }} + /> + ) +} +WithPaginationAsTable.parameters = { + docs: { + canvas: { + sourceState: "none", + }, + }, +} From 651fff95beb3b3f63e9690cc39f9ba41fa241969 Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:43:55 +0100 Subject: [PATCH 2/7] feat: add PaginationInfo component and integrate it into useResourceList --- .../useResourceList/PaginationInfo.tsx | 57 ++++++++++++ .../useResourceList/infiniteFetcher.ts | 91 ------------------- .../useResourceList/useResourceList.tsx | 58 ++++++------ 3 files changed, 88 insertions(+), 118 deletions(-) create mode 100644 packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx delete mode 100644 packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts diff --git a/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx new file mode 100644 index 000000000..8e990ab16 --- /dev/null +++ b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx @@ -0,0 +1,57 @@ +import type { JSX } from "react" +import { Icon } from "#ui/atoms/Icon" +import { Spacer } from "#ui/atoms/Spacer" +import { Text } from "#ui/atoms/Text" +import { makeCurrentPageOffsets } from "#utils/pagination" + +export interface PaginationInfoProps { + currentPage: number + pageCount: number + recordsPerPage: number + recordCount: number + isLoading: boolean + onPageChange: (page: number) => void +} + +export function PaginationInfo({ + currentPage, + pageCount, + recordsPerPage, + recordCount, + isLoading, + onPageChange, +}: PaginationInfoProps): JSX.Element { + const offsets = makeCurrentPageOffsets({ + currentPage, + recordsPerPage, + recordCount, + }) + + return ( + +
+ + {offsets.firstOfPage}-{offsets.lastOfPage} of {recordCount} + +
+ + +
+
+
+ ) +} diff --git a/packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts b/packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts deleted file mode 100644 index 6cd5530c1..000000000 --- a/packages/app-elements/src/ui/resources/useResourceList/infiniteFetcher.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { - CommerceLayerClient, - ListableResourceType, - QueryParamsList, - ResourceFields, -} from "@commercelayer/sdk" -import uniqBy from "lodash-es/uniqBy" -import { - isValidMetricsResource, - type MetricsApiClient, - type MetricsResources, -} from "./metricsApiClient" - -type ListResource = Awaited< - ReturnType -> - -export type Resource = - ListResource[number] - -export interface FetcherResponse { - list: TResource[] - meta: { - pageCount: number - recordCount: number - currentPage: number - recordsPerPage: number - cursor?: string | null - } -} - -export async function listFetcher({ - currentData, - resourceType, - client, - clientType, - query, - mode = "infinite", - pageNumber, -}: { - currentData?: FetcherResponse> - resourceType: TResource - mode?: "infinite" | "pagination" - pageNumber?: number -} & ( - | { - client: CommerceLayerClient - clientType: "coreSdkClient" - query?: Omit, "pageNumber"> - } - | { - client: MetricsApiClient - clientType: "metricsClient" - query: Record> - } -)): Promise>> { - const currentPage = currentData?.meta.currentPage ?? 0 - const pageToFetch = - mode === "pagination" && pageNumber != null ? pageNumber : currentPage + 1 - - if (clientType === "metricsClient" && !isValidMetricsResource(resourceType)) { - throw new Error("Metrics client is not available for this resource type") - } - - const listResponse = - clientType === "metricsClient" - ? await client.list(resourceType as MetricsResources, { - ...query, - search: { - ...query.search, - cursor: currentData?.meta.cursor ?? null, - }, - }) - : // @ts-expect-error "Expression produces a union type that is too complex to represent" - await client[resourceType].list({ - ...query, - pageNumber: pageToFetch, - }) - - // we need the primitive array - // without the sdk added methods ('meta' | 'first' | 'last' | 'get') - const existingList = currentData?.list ?? [] - // In pagination mode, replace the list instead of accumulating - const uniqueList = - mode === "pagination" - ? [...listResponse] - : uniqBy(existingList.concat(listResponse), "id") - const meta = listResponse.meta - - return { list: uniqueList, meta } -} diff --git a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx index 10bc57a3a..461b3578e 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx @@ -20,7 +20,6 @@ import { t } from "#providers/I18NProvider" import { Button } from "#ui/atoms/Button" import { Card } from "#ui/atoms/Card" import { EmptyState } from "#ui/atoms/EmptyState" -import { Pagination } from "#ui/atoms/Pagination" import { Section, type SectionProps } from "#ui/atoms/Section" import { SkeletonTemplate, @@ -33,6 +32,7 @@ import { Text } from "#ui/atoms/Text" import { InputFeedback } from "#ui/forms/InputFeedback" import { listFetcher, type Resource } from "./listFetcher" import { useMetricsSdkProvider } from "./metricsApiClient" +import { PaginationInfo } from "./PaginationInfo" import { initialState, reducer } from "./reducer" import { computeTitleWithTotalCount } from "./utils" import { VisibilityTrigger } from "./VisibilityTrigger" @@ -355,33 +355,24 @@ export function useResourceList({ }) }} /> - ) : isLoading ? ( - Array(isFirstLoading ? 8 : 2) // we want more elements as skeleton on first mount - .fill(null) - .map((_, idx) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: Using index as key is acceptable here since items are static - - )) - ) : paginationType === "pagination" && - data != null && - data.meta.pageCount > 1 ? ( - - - ) : paginationType === "infinite" ? ( - { - if (entry.isIntersecting) { - void fetchMore({ query }) - } - }} - /> + isLoading ? ( + Array(isFirstLoading ? 8 : 2) // we want more elements as skeleton on first mount + .fill(null) + .map((_, idx) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: Using index as key is acceptable here since items are static + + )) + ) : ( + { + if (entry.isIntersecting) { + void fetchMore({ query }) + } + }} + /> + ) ) : null } > @@ -397,6 +388,19 @@ export function useResourceList({ ) })} + + {paginationType === "pagination" && + data != null && + data.meta.pageCount > 1 ? ( + + ) : null} ) From 68cdedf69a0bef8062e97c661ea7dcce01f6347e Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:43:33 +0100 Subject: [PATCH 3/7] feat: enhance pagination support by adding Pagination component to useResourceList and stories --- .../useResourceFilters/useResourceFilters.tsx | 29 ++++-- .../useResourceList/PaginationInfo.tsx | 4 +- .../useResourceList/useResourceList.tsx | 98 ++++++++++++++----- .../resources/useResourceList.stories.tsx | 84 ++++++++-------- 4 files changed, 142 insertions(+), 73 deletions(-) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/useResourceFilters.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/useResourceFilters.tsx index d390784ca..e8caf3922 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/useResourceFilters.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/useResourceFilters.tsx @@ -7,7 +7,10 @@ import { type UseResourceListConfig, useResourceList, } from "#ui/resources/useResourceList" -import type { ResourceListProps } from "#ui/resources/useResourceList/useResourceList" +import type { + ResourceListProps, + UseResourceListReturnWithPagination, +} from "#ui/resources/useResourceList/useResourceList" import { makeFilterAdapters } from "./adapters" import { FiltersForm as FiltersFormComponent, @@ -193,17 +196,29 @@ function ResourceListComponent({ metricsQuery, type, query, + paginationType = "infinite", ...listProps -}: UseResourceListConfig & - ResourceListProps): JSX.Element { - const hookConfig: UseResourceListConfig = { +}: UseResourceListConfig & { + paginationType?: "infinite" | "pagination" +} & ResourceListProps): JSX.Element { + const result = useResourceList({ type, query, metricsQuery, - } - const { ResourceList } = useResourceList(hookConfig) + paginationType, + }) - return + const paginationResult = + paginationType === "pagination" + ? (result as UseResourceListReturnWithPagination) + : null + + return ( + <> + + {paginationResult != null && } + + ) } const makeFilteredList: (options: { diff --git a/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx index 8e990ab16..c54ba103a 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx @@ -31,7 +31,9 @@ export function PaginationInfo({
- {offsets.firstOfPage}-{offsets.lastOfPage} of {recordCount} + {offsets.firstOfPage.toLocaleString()}- + {offsets.lastOfPage.toLocaleString()} of{" "} + {recordCount.toLocaleString()}
} - actionButton={} - ItemTemplate={({ resource = mockedOrder, isLoading }) => { - return ( - - - - ) - }} - /> + <> + Empty
} + actionButton={} + ItemTemplate={({ resource = mockedOrder, isLoading }) => { + return ( + + + + ) + }} + /> + + ) } WithPagination.parameters = { @@ -255,7 +258,7 @@ WithPagination.parameters = { * Pagination mode also works with table variant. */ export const WithPaginationAsTable: StoryFn = () => { - const { ResourceList } = useResourceList({ + const { ResourceList, Pagination } = useResourceList({ type: "orders", query: { pageSize: 10, @@ -264,31 +267,34 @@ export const WithPaginationAsTable: StoryFn = () => { }) return ( - - Order - - } - ItemTemplate={({ resource = mockedOrder, isLoading }) => { - return ( - - #{resource.number} - {resource.market?.name} - - {resource.formatted_total_amount} - - - ) - }} - /> + <> + + Order + + } + ItemTemplate={({ resource = mockedOrder, isLoading }) => { + return ( + + #{resource.number} + {resource.market?.name} + + {resource.formatted_total_amount} + + + ) + }} + /> + + ) } WithPaginationAsTable.parameters = { From 7abd76f96633c8efaff15c94325100190d7df4e2 Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:57:38 +0100 Subject: [PATCH 4/7] feat: smooth scroll to top on page change in useResourceList --- .../src/ui/resources/useResourceList/useResourceList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx index d70f85750..a228ce358 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx @@ -285,6 +285,7 @@ export function useResourceList({ (newPage: number) => { setCurrentPage(newPage) void fetchMore({ query, pageNumber: newPage }) + window.scrollTo({ top: 0, behavior: "smooth" }) }, [query, fetchMore], ) From cb88f7e531b498491484e6dfcc660f80073e7ff3 Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:17:04 +0100 Subject: [PATCH 5/7] fix: change scroll behavior to instant on page change in useResourceList --- .../src/ui/resources/useResourceList/useResourceList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx index a228ce358..2e990192b 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx @@ -285,7 +285,7 @@ export function useResourceList({ (newPage: number) => { setCurrentPage(newPage) void fetchMore({ query, pageNumber: newPage }) - window.scrollTo({ top: 0, behavior: "smooth" }) + window.scrollTo({ top: 0, behavior: "instant" }) }, [query, fetchMore], ) From 82a3b41fcbc1a919c4bff403bac79ab90405d0ab Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:18:23 +0100 Subject: [PATCH 6/7] fix: enhance PaginationInfo component with accessibility improvements and smooth scrolling on page change --- .../useResourceList/PaginationInfo.tsx | 26 +++++++++++++++---- .../useResourceList/useResourceList.tsx | 11 +++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx index c54ba103a..477161d77 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx @@ -30,7 +30,13 @@ export function PaginationInfo({ return (
- + {offsets.firstOfPage.toLocaleString()}- {offsets.lastOfPage.toLocaleString()} of{" "} {recordCount.toLocaleString()} @@ -38,19 +44,29 @@ export function PaginationInfo({
diff --git a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx index 2e990192b..08762900c 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx @@ -279,13 +279,13 @@ export function useResourceList({ query, pageNumber: paginationType === "pagination" ? 1 : undefined, }) - }, [query, paginationType]) + }, [query, paginationType, fetchMore]) const handlePageChange = useCallback( (newPage: number) => { setCurrentPage(newPage) void fetchMore({ query, pageNumber: newPage }) - window.scrollTo({ top: 0, behavior: "instant" }) + window.scrollTo({ top: 0 }) }, [query, fetchMore], ) @@ -433,7 +433,6 @@ export function useResourceList({ error, paginationType, currentPage, - handlePageChange, query, fetchMore, ], @@ -450,7 +449,7 @@ export function useResourceList({ return ( ({ removeItem, refresh, fetchMore: async () => { + if (paginationType === "pagination") { + console.warn("fetchMore is not supported in pagination mode.") + return + } if (hasMorePages) { await fetchMore({ query }) } From d1f09f7257d0d4041d97a554dd7eed15ac6e000c Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:51:40 +0100 Subject: [PATCH 7/7] fix: update pagination logic to ensure correct page number in order IDs and improve scroll behavior on page change --- packages/app-elements/src/mocks/handlers.ts | 6 +- .../useResourceList/PaginationInfo.tsx | 2 - .../resources/useResourceList/listFetcher.ts | 2 +- .../useResourceList/useResourceList.test.tsx | 96 ++++++++++++++++++- .../useResourceList/useResourceList.tsx | 5 +- 5 files changed, 103 insertions(+), 8 deletions(-) diff --git a/packages/app-elements/src/mocks/handlers.ts b/packages/app-elements/src/mocks/handlers.ts index 813be6e1f..9554d34f0 100644 --- a/packages/app-elements/src/mocks/handlers.ts +++ b/packages/app-elements/src/mocks/handlers.ts @@ -77,8 +77,10 @@ export const handlers = [ }), http.get(`https://*/api/orders`, async ({ request }) => { + const url = new URL(request.url) + const pageNumber = Number(url.searchParams.get("page[number]") ?? "1") return HttpResponse.json( - returnEmptyList(new URL(request.url)) + returnEmptyList(url) ? { data: [], meta: { record_count: 0, page_count: 1 }, @@ -87,7 +89,7 @@ export const handlers = [ data: Array(10) .fill(null) .map(() => ({ - id: Math.random().toString().substring(2, 12), + id: `page${pageNumber}-${Math.random().toString().substring(2, 12)}`, type: "orders", attributes: { number: Math.floor(Math.random() * 100_000), diff --git a/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx index 477161d77..47ed0d808 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/PaginationInfo.tsx @@ -48,7 +48,6 @@ export function PaginationInfo({ aria-disabled={isLoading || currentPage === 1} disabled={isLoading || currentPage === 1} onClick={() => { - window.scrollTo({ top: 0, behavior: "smooth" }) onPageChange(currentPage - 1) }} className="p-2 disabled:opacity-30 rounded-[8px] border border-gray-200" @@ -61,7 +60,6 @@ export function PaginationInfo({ aria-disabled={isLoading || currentPage === pageCount} disabled={isLoading || currentPage === pageCount} onClick={() => { - window.scrollTo({ top: 0, behavior: "smooth" }) onPageChange(currentPage + 1) }} className="p-2 disabled:opacity-30 rounded-[8px] border border-gray-200" diff --git a/packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts b/packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts index 6cd5530c1..9566a6dc8 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts +++ b/packages/app-elements/src/ui/resources/useResourceList/listFetcher.ts @@ -87,5 +87,5 @@ export async function listFetcher({ : uniqBy(existingList.concat(listResponse), "id") const meta = listResponse.meta - return { list: uniqueList, meta } + return { list: uniqueList, meta: { ...meta, currentPage: pageToFetch } } } diff --git a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.test.tsx b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.test.tsx index b082b93c7..4c2ad476e 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.test.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.test.tsx @@ -1,5 +1,5 @@ import type { Order } from "@commercelayer/sdk" -import { render } from "@testing-library/react" +import { render, waitFor } from "@testing-library/react" import { act, type FC } from "react" import { CoreSdkProvider } from "#providers/CoreSdkProvider" import { MockTokenProvider as TokenProvider } from "#providers/TokenProvider/MockTokenProvider" @@ -44,6 +44,36 @@ const ResourceListImplementation: FC< ) } +const PaginationListImplementation: FC = () => { + const { ResourceList, Pagination } = useResourceList({ + type: "orders", + paginationType: "pagination", + }) + + return ( + <> + ( +
+ Order #{resource.number} +
+ )} + /> + + + ) +} + describe("useResourceList", () => { test("Should render list component", async () => { const { getByTestId } = render( @@ -118,3 +148,67 @@ describe("useResourceList", () => { expect(await findByText("No orders found")).toBeVisible() }) }) + +describe("useResourceList - pagination mode", () => { + test("Should replace items (not accumulate) when navigating pages", async () => { + const { findAllByTestId, findByRole } = render( + + + + + , + ) + + // Wait for page 1 to load + const page1Items = await findAllByTestId("orderItem-ready") + expect(page1Items.length).toBe(10) + expect(page1Items[0]?.dataset.page).toBe("page1") + + // Navigate to page 2 (wait for button to be enabled) + const nextButton = await findByRole("button", { name: "Next page" }) + await act(async () => { + nextButton.click() + }) + + // Wait for page 2 items — list should be replaced, not grown + await waitFor(async () => { + const page2Items = await findAllByTestId("orderItem-ready") + expect(page2Items.length).toBe(10) + expect(page2Items[0]?.dataset.page).toBe("page2") + }) + }) + + test("Should disable prev button on first page and next button on last page", async () => { + const { findByRole } = render( + + + + + , + ) + + // Wait for buttons to appear (Pagination renders only after data is loaded) + await waitFor(async () => { + const prevButton = await findByRole("button", { name: "Previous page" }) + const nextButton = await findByRole("button", { name: "Next page" }) + expect(prevButton).toBeDisabled() + expect(nextButton).not.toBeDisabled() + }) + + // Navigate to last page (page_count is 2) + const nextButton = await findByRole("button", { name: "Next page" }) + await act(async () => { + nextButton.click() + }) + + // On page 2 (last): next should be disabled, prev enabled + await waitFor(async () => { + const prevButton = await findByRole("button", { name: "Previous page" }) + const nextButtonUpdated = await findByRole("button", { + name: "Next page", + }) + expect(nextButtonUpdated).toBeDisabled() + expect(prevButton).not.toBeDisabled() + }) + }) +}) diff --git a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx index 08762900c..1549f0ede 100644 --- a/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx +++ b/packages/app-elements/src/ui/resources/useResourceList/useResourceList.tsx @@ -284,8 +284,9 @@ export function useResourceList({ const handlePageChange = useCallback( (newPage: number) => { setCurrentPage(newPage) - void fetchMore({ query, pageNumber: newPage }) - window.scrollTo({ top: 0 }) + void fetchMore({ query, pageNumber: newPage }).then(() => { + window.scrollTo({ top: 0 }) + }) }, [query, fetchMore], )