Skip to content

Commit

Permalink
clients/web: improve ProductsSelect to handle paginated selections
Browse files Browse the repository at this point in the history
  • Loading branch information
frankie567 committed Jan 6, 2025
1 parent 2cdf675 commit 36dd864
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 36 deletions.
64 changes: 32 additions & 32 deletions clients/apps/web/src/components/Products/ProductSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useProducts } from '@/hooks/queries'
import { useProducts, useSelectedProducts } from '@/hooks/queries'
import { CheckOutlined, ExpandMoreOutlined } from '@mui/icons-material'
import {
Organization,
Expand Down Expand Up @@ -89,36 +89,43 @@ const ProductSelect: React.FC<ProductSelectProps> = ({
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<ProductsApiListRequest, 'organizationId'>
>(
() => ({
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<Record<ProductPriceType, Product[]>>(() => {
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<Record<ProductPriceType, Product[]>>(
return products
}, [queriedProducts, selectedProducts, query])

// Group displayed products by product price type
const groupedProducts = useMemo<Record<ProductPriceType, Product[]>>(() => {
return displayedProducts.reduce<Record<ProductPriceType, Product[]>>(
(acc, product) => {
const type = product.is_recurring
? ProductPriceType.RECURRING
Expand All @@ -133,24 +140,17 @@ const ProductSelect: React.FC<ProductSelectProps> = ({
[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) => {
Expand Down Expand Up @@ -211,22 +211,22 @@ const ProductSelect: React.FC<ProductSelectProps> = ({
onValueChange={setQuery}
/>
<CommandList>
{filteredProducts?.items && filteredProducts.items.length ? (
{queriedProducts?.items && queriedProducts.items.length ? (
<>
<ProductsCommandGroup
groupedProducts={groupedProducts}
productPriceType={ProductPriceType.ONE_TIME}
onSelectProduct={onSelectProduct}
onSelectProductType={onSelectProductType}
selectedProducts={selectedProducts}
selectedProducts={selectedProducts || []}
/>
<CommandSeparator />
<ProductsCommandGroup
groupedProducts={groupedProducts}
productPriceType={ProductPriceType.RECURRING}
onSelectProduct={onSelectProduct}
onSelectProductType={onSelectProductType}
selectedProducts={selectedProducts}
selectedProducts={selectedProducts || []}
className="!mt-0"
/>
</>
Expand Down
34 changes: 30 additions & 4 deletions clients/apps/web/src/hooks/queries/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,54 @@ 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<ProductsApiListRequest, 'organizationId'>,
) =>
useQuery({
queryKey: ['products', { organizationId, ...(parameters || {}) }],
queryFn: () =>
api.products.list({
organizationId: organizationId ?? '',
organizationId,
isArchived: false,
...(parameters || {}),
}),
retry: defaultRetry,
enabled: !!organizationId,
})

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 = (
Expand Down

0 comments on commit 36dd864

Please sign in to comment.