From c66df31e00dd89afac823224be9373cd012c2d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Mon, 6 Jan 2025 14:31:01 +0100 Subject: [PATCH] clients/web: improve ProductsSelect to handle paginated selections --- .../src/components/Products/ProductSelect.tsx | 64 +++++++++---------- .../apps/web/src/hooks/queries/products.ts | 36 +++++++++-- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/clients/apps/web/src/components/Products/ProductSelect.tsx b/clients/apps/web/src/components/Products/ProductSelect.tsx index 9700c6fa58..eaf5c55a13 100644 --- a/clients/apps/web/src/components/Products/ProductSelect.tsx +++ b/clients/apps/web/src/components/Products/ProductSelect.tsx @@ -1,4 +1,4 @@ -import { useProducts } from '@/hooks/queries' +import { useProducts, useSelectedProducts } from '@/hooks/queries' import { CheckOutlined, ExpandMoreOutlined } from '@mui/icons-material' import { Organization, @@ -89,36 +89,43 @@ const ProductSelect: React.FC = ({ const [open, setOpen] = useState(false) const [query, setQuery] = useState('') - // All products to maintain the selection even if the product is not in the filtered list - const { data: allProducts } = useProducts(organization.id, { - isArchived: false, - }) + // Selected products, with a dedicated hook to exhaust pagination + const { data: selectedProducts } = useSelectedProducts(value) - // Filtered products based on the query - const productslistParameters = useMemo< + // Queried products, show a selection of products based on the query + const queriedProductsParameters = useMemo< Omit >( () => ({ isArchived: false, ...(query ? { query } : {}), sorting: ['name'], + limit: 20, }), [query], ) - const { data: filteredProducts } = useProducts( + const { data: queriedProducts } = useProducts( organization.id, - productslistParameters, + queriedProductsParameters, ) - // Group filtered products by product price type - const groupedProducts = useMemo>(() => { - if (!filteredProducts) { - return { - [ProductPriceType.ONE_TIME]: [], - [ProductPriceType.RECURRING]: [], + // Products displayed in the selector + const displayedProducts = useMemo(() => { + let products = queriedProducts?.items || [] + // If no current query, show selected products no matter what + if (!query) { + for (const product of selectedProducts || []) { + if (!products.some(({ id }) => id === product.id)) { + products = [...products, product] + } } } - return filteredProducts.items.reduce>( + return products + }, [queriedProducts, selectedProducts, query]) + + // Group displayed products by product price type + const groupedProducts = useMemo>(() => { + return displayedProducts.reduce>( (acc, product) => { const type = product.is_recurring ? ProductPriceType.RECURRING @@ -133,24 +140,17 @@ const ProductSelect: React.FC = ({ [ProductPriceType.RECURRING]: [], }, ) - }, [filteredProducts]) - - const selectedProducts = useMemo(() => { - if (!allProducts) { - return [] - } - return allProducts.items.filter((product) => value.includes(product.id)) - }, [allProducts, value]) + }, [displayedProducts]) const buttonLabel = useMemo(() => { - if (selectedProducts.length === 0) { + if (value.length === 0) { return 'All products' } - if (selectedProducts.length === 1) { - return selectedProducts[0].name + if (value.length === 1) { + return selectedProducts?.[0].name } - return `${selectedProducts.length} products` - }, [selectedProducts]) + return `${value.length} products` + }, [value, selectedProducts]) const onSelectProduct = useCallback( (product: Product) => { @@ -211,14 +211,14 @@ const ProductSelect: React.FC = ({ onValueChange={setQuery} /> - {filteredProducts?.items && filteredProducts.items.length ? ( + {queriedProducts?.items && queriedProducts.items.length ? ( <> = ({ productPriceType={ProductPriceType.RECURRING} onSelectProduct={onSelectProduct} onSelectProductType={onSelectProductType} - selectedProducts={selectedProducts} + selectedProducts={selectedProducts || []} className="!mt-0" /> diff --git a/clients/apps/web/src/hooks/queries/products.ts b/clients/apps/web/src/hooks/queries/products.ts index f0e1f23b4b..16869e17af 100644 --- a/clients/apps/web/src/hooks/queries/products.ts +++ b/clients/apps/web/src/hooks/queries/products.ts @@ -3,28 +3,56 @@ import { api, queryClient } from '@/utils/api' import { Organization, OrganizationIDFilter1, + Product, ProductBenefitsUpdate, ProductCreate, ProductUpdate, ProductsApiListRequest, } from '@polar-sh/sdk' -import { useMutation, useQuery } from '@tanstack/react-query' +import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query' import { defaultRetry } from './retry' export const useProducts = ( - organizationId?: OrganizationIDFilter1, + organizationId: OrganizationIDFilter1, parameters?: Omit, + enabled = true, ) => useQuery({ queryKey: ['products', { organizationId, ...(parameters || {}) }], queryFn: () => api.products.list({ - organizationId: organizationId ?? '', + organizationId, isArchived: false, ...(parameters || {}), }), retry: defaultRetry, - enabled: !!organizationId, + enabled, + }) + +export const useSelectedProducts = (id: string[]) => + useQuery({ + queryKey: ['products', { id }], + queryFn: async () => { + const products: Product[] = [] + let page = 1 + while (true) { + const data = await api.products.list({ + id, + isArchived: false, + page, + limit: 1, + }) + products.push(...data.items) + if (data.pagination.max_page === page) { + break + } + page++ + } + return products + }, + placeholderData: keepPreviousData, + retry: defaultRetry, + enabled: id.length > 0, }) export const useBenefitProducts = (