diff --git a/inc/Integrations/Shopify/Queries/SearchProducts.graphql b/inc/Integrations/Shopify/Queries/SearchProducts.graphql index 7666fcd3..4711b3c6 100644 --- a/inc/Integrations/Shopify/Queries/SearchProducts.graphql +++ b/inc/Integrations/Shopify/Queries/SearchProducts.graphql @@ -1,5 +1,16 @@ -query SearchProducts($search: String) { - products(first: 10, query: $search, sortKey: BEST_SELLING) { +query SearchProducts($search: String!, $limit: Int!, $cursor_next: String) { + products( + first: $limit + after: $cursor_next + query: $search + sortKey: BEST_SELLING + ) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } edges { node { id diff --git a/inc/Integrations/Shopify/ShopifyIntegration.php b/inc/Integrations/Shopify/ShopifyIntegration.php index 52cd75af..cb3612dd 100644 --- a/inc/Integrations/Shopify/ShopifyIntegration.php +++ b/inc/Integrations/Shopify/ShopifyIntegration.php @@ -86,6 +86,19 @@ public static function get_queries( ShopifyDataSource $data_source ): array { 'search' => [ 'type' => 'ui:search_input', ], + 'limit' => [ + 'default_value' => 8, + 'name' => 'Items per page', + 'type' => 'ui:pagination_per_page', + ], + 'cursor_next' => [ + 'name' => 'Next page cursor', + 'type' => 'ui:pagination_cursor_next', + ], + 'cursor_previous' => [ + 'name' => 'Previous page cursor', + 'type' => 'ui:pagination_cursor_previous', + ], ], 'output_schema' => [ 'path' => '$.data.products.edges[*]', @@ -113,6 +126,23 @@ public static function get_queries( ShopifyDataSource $data_source ): array { ], ], ], + 'pagination_schema' => [ + 'cursor_next' => [ + 'name' => 'Next page cursor', + 'path' => '$.data.products.pageInfo.endCursor', + 'type' => 'string', + ], + 'cursor_previous' => [ + 'name' => 'Previous page cursor', + 'path' => '$.data.products.pageInfo.startCursor', + 'type' => 'string', + ], + 'has_next_page' => [ + 'name' => 'Has next page', + 'path' => '$.data.products.pageInfo.hasNextPage', + 'type' => 'boolean', + ], + ], 'graphql_query' => file_get_contents( __DIR__ . '/Queries/SearchProducts.graphql' ), ] ), ]; diff --git a/inc/Validation/ConfigSchemas.php b/inc/Validation/ConfigSchemas.php index 45c5b2a4..aa18737b 100644 --- a/inc/Validation/ConfigSchemas.php +++ b/inc/Validation/ConfigSchemas.php @@ -288,14 +288,15 @@ private static function generate_http_query_config_schema(): array { 'pagination_schema' => Types::nullable( Types::object( [ // This field provides an integer representing the total number of - // items available in paginated results. This field must be defined - // in order present to enable pagination support of any type, - // including cursor-based pagination. - 'total_items' => Types::object( [ - 'name' => Types::nullable( Types::string() ), - 'path' => Types::json_path(), - 'type' => Types::enum( 'integer' ), - ] ), + // items available in paginated results. Either this field or + // `has_next_page` must be defined in order to enable pagination. + 'total_items' => Types::nullable( + Types::object( [ + 'name' => Types::nullable( Types::string() ), + 'path' => Types::json_path(), + 'type' => Types::enum( 'integer' ), + ] ), + ), // This field provides a pagination cursor for the next page of // paginated results, or a null value if there is no next page. This // field must be defined in order to enable cursor-based pagination. @@ -316,6 +317,16 @@ private static function generate_http_query_config_schema(): array { 'type' => Types::enum( 'string' ), ] ), ), + // This field provides a boolean indicating if there is a next page of + // paginated results. This is helpful if the API does not provide a + // total number of items. + 'has_next_page' => Types::nullable( + Types::object( [ + 'name' => Types::nullable( Types::string() ), + 'path' => Types::json_path(), + 'type' => Types::enum( 'boolean' ), + ] ) + ), ] ) ), 'preprocess_response' => Types::nullable( Types::callable() ), diff --git a/src/blocks/remote-data-container/components/item-list/ItemList.tsx b/src/blocks/remote-data-container/components/item-list/ItemList.tsx index 6ba90c70..0e6fe7e9 100644 --- a/src/blocks/remote-data-container/components/item-list/ItemList.tsx +++ b/src/blocks/remote-data-container/components/item-list/ItemList.tsx @@ -27,6 +27,7 @@ function getResultsWithId( results: RemoteDataResult[], instanceId: string ): Re interface ItemListProps { availableBindings: Record< string, RemoteDataBinding >; blockName: string; + hasNextPage: boolean; loading: boolean; onSelect: ( data: RemoteDataQueryInput ) => void; onSelectField?: ( data: FieldSelection, fieldValue: string ) => void; @@ -35,6 +36,7 @@ interface ItemListProps { remoteData?: RemoteData; searchInput: string; setPage: ( newPage: number ) => void; + setPerPage: ( newPerPage: number ) => void; setSearchInput: ( newValue: string ) => void; supportsSearch: boolean; totalItems?: number; @@ -45,6 +47,7 @@ export function ItemList( props: ItemListProps ) { const { availableBindings, blockName, + hasNextPage, loading, onSelect, onSelectField, @@ -53,6 +56,7 @@ export function ItemList( props: ItemListProps ) { remoteData, searchInput, setPage, + setPerPage, setSearchInput, supportsSearch, totalItems, @@ -121,6 +125,7 @@ export function ItemList( props: ItemListProps ) { function onChangeView( newView: View ) { setPage( newView.page ?? 1 ); + setPerPage( newView.perPage ?? perPage ?? data.length ); setSearchInput( newView.search ?? '' ); setView( newView ); } @@ -157,7 +162,7 @@ export function ItemList( props: ItemListProps ) { onChangeView={ onChangeView } paginationInfo={ { totalItems: totalItems ?? data.length, - totalPages: totalPages ?? 1, + totalPages: totalPages ?? ( hasNextPage ? page + 1 : page - 1 ) ?? 1, } } search={ supportsSearch } view={ view } diff --git a/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx b/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx index 1bd55b0a..0d6bfe23 100644 --- a/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx +++ b/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx @@ -31,10 +31,12 @@ export const DataViewsModal: React.FC< DataViewsModalProps > = props => { const { close, isOpen, open } = useModalState(); const { data, + hasNextPage, loading, page, searchInput, setPage, + setPerPage, setSearchInput, supportsSearch, totalItems, @@ -72,6 +74,7 @@ export const DataViewsModal: React.FC< DataViewsModalProps > = props => { = props => { remoteData={ data } searchInput={ searchInput } setPage={ setPage } + setPerPage={ setPerPage } setSearchInput={ setSearchInput } supportsSearch={ supportsSearch } totalItems={ totalItems } diff --git a/src/blocks/remote-data-container/hooks/usePaginationVariables.ts b/src/blocks/remote-data-container/hooks/usePaginationVariables.ts index d4cdf536..51beb5bf 100644 --- a/src/blocks/remote-data-container/hooks/usePaginationVariables.ts +++ b/src/blocks/remote-data-container/hooks/usePaginationVariables.ts @@ -9,6 +9,7 @@ import { } from '@/blocks/remote-data-container/config/constants'; interface UsePaginationVariables { + hasNextPage?: boolean; onFetch: ( remoteData: RemoteData ) => void; page: number; paginationQueryInput: RemoteDataQueryInput; @@ -88,6 +89,7 @@ export function usePaginationVariables( { supportsCursorPagination || supportsPagePagination || supportsOffsetPagination; const totalItems = paginationData?.totalItems; const totalPages = totalItems && perPage ? Math.ceil( totalItems / perPage ) : undefined; + const hasNextPage = paginationData?.hasNextPage; function onFetch( remoteData: RemoteData ): void { if ( ! supportsPagination ) { @@ -120,6 +122,7 @@ export function usePaginationVariables( { } return { + hasNextPage, onFetch, page, paginationQueryInput, diff --git a/src/blocks/remote-data-container/hooks/useRemoteData.ts b/src/blocks/remote-data-container/hooks/useRemoteData.ts index 866e138a..5a2b5da9 100644 --- a/src/blocks/remote-data-container/hooks/useRemoteData.ts +++ b/src/blocks/remote-data-container/hooks/useRemoteData.ts @@ -31,6 +31,7 @@ async function fetchRemoteData( requestData: RemoteDataApiRequest ): Promise< Re pagination: body.pagination && { cursorNext: body.pagination.cursor_next, cursorPrevious: body.pagination.cursor_previous, + hasNextPage: body.pagination.has_next_page, totalItems: body.pagination.total_items, }, queryInput: body.query_input, @@ -51,7 +52,7 @@ interface UseRemoteData { data?: RemoteData; error?: Error; fetch: ( queryInput: RemoteDataQueryInput ) => Promise< void >; - hasNextPage: boolean; + hasNextPage?: boolean; hasPreviousPage: boolean; loading: boolean; page: number; @@ -126,6 +127,7 @@ export function useRemoteData( { const inputVariables = query.inputs; const { + hasNextPage, onFetch: onFetchForPagination, page, perPage, @@ -237,7 +239,7 @@ export function useRemoteData( { data: resolvedData, error, fetch, - hasNextPage: totalPages ? page < totalPages : supportsPagination, + hasNextPage: hasNextPage ?? ( totalPages ? page < totalPages : supportsPagination ), hasPreviousPage: page > 1, loading, page, diff --git a/types/remote-data.d.ts b/types/remote-data.d.ts index 082e1f82..1fdd288b 100644 --- a/types/remote-data.d.ts +++ b/types/remote-data.d.ts @@ -5,7 +5,8 @@ interface InnerBlockContext { interface RemoteDataPagination { cursorNext?: string; cursorPrevious?: string; - totalItems: number; + hasNextPage?: boolean; + totalItems?: number; } interface RemoteDataResultFields { @@ -87,7 +88,8 @@ interface RemoteDataApiResponseBody { pagination?: { cursor_next?: string; cursor_previous?: string; - total_items: number; + has_next_page?: boolean; + total_items?: number; }; query_input: RemoteDataQueryInput; result_id: string;