@graphcommerce/magento-product 8.1.0-canary.8 → 9.0.0-canary.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/Api/ProductListItem.graphql +1 -2
  2. package/Api/ProductPageItem.graphql +1 -1
  3. package/CHANGELOG.md +278 -84
  4. package/Config.graphqls +13 -0
  5. package/components/AddProductsToCart/AddProductsToCartButton.tsx +17 -4
  6. package/components/AddProductsToCart/AddProductsToCartFab.tsx +7 -2
  7. package/components/AddProductsToCart/AddProductsToCartForm.tsx +31 -29
  8. package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +14 -63
  9. package/components/AddProductsToCart/AddProductsToCartSnackbarMessage.tsx +84 -0
  10. package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
  11. package/components/AddProductsToCart/findAddedItems.ts +1 -4
  12. package/components/AddProductsToCart/index.ts +1 -0
  13. package/components/AddProductsToCart/useAddProductsToCartAction.ts +2 -1
  14. package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
  15. package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
  16. package/components/JsonLdProduct/ProductPageJsonLd.tsx +1 -1
  17. package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
  18. package/components/ProductCustomizable/CustomizableCheckboxOption.tsx +3 -4
  19. package/components/ProductCustomizable/CustomizableMultipleOption.tsx +2 -2
  20. package/components/ProductCustomizable/CustomizableRadioOption.tsx +2 -2
  21. package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
  22. package/components/ProductCustomizable/index.ts +1 -0
  23. package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
  24. package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
  25. package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +4 -5
  26. package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +6 -7
  27. package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
  28. package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
  29. package/components/ProductFiltersPro/ProductFiltersPro.tsx +103 -19
  30. package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +41 -20
  31. package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -10
  32. package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +18 -8
  33. package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +130 -0
  34. package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
  35. package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
  36. package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
  37. package/components/ProductFiltersPro/ProductFiltersProLimitChip.tsx +2 -8
  38. package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +7 -10
  39. package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
  40. package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +5 -7
  41. package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +2 -4
  42. package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +11 -3
  43. package/components/ProductFiltersPro/activeAggregations.ts +5 -9
  44. package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
  45. package/components/ProductFiltersPro/index.ts +9 -0
  46. package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
  47. package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
  48. package/components/ProductFiltersPro/useProductFiltersProSort.tsx +7 -3
  49. package/components/ProductList/ProductList.graphql +8 -5
  50. package/components/ProductListCount/ProductListCount.tsx +3 -1
  51. package/components/ProductListFilters/ProductFilters.graphql +11 -2
  52. package/components/ProductListFilters/ProductListFilters.graphql +1 -1
  53. package/components/ProductListFilters/ProductListFilters.tsx +13 -19
  54. package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
  55. package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
  56. package/components/ProductListItem/ProductListItem.tsx +3 -3
  57. package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
  58. package/components/ProductListItems/ProductFilterTypes.graphql +8 -0
  59. package/components/ProductListItems/ProductListItemsBase.tsx +71 -30
  60. package/components/ProductListItems/filterTypes.tsx +14 -7
  61. package/components/ProductListItems/filteredProductList.tsx +44 -17
  62. package/components/ProductListItems/getFilterTypes.ts +33 -4
  63. package/components/ProductListItems/productListApplyCategoryDefaults.ts +50 -4
  64. package/components/ProductListItems/renderer.tsx +8 -2
  65. package/components/ProductListPagination/ProductListPagination.tsx +39 -20
  66. package/components/ProductListPrice/ProductListPrice.tsx +9 -4
  67. package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
  68. package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
  69. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
  70. package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +3 -0
  71. package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
  72. package/components/ProductPageBreadcrumb/index.ts +1 -0
  73. package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
  74. package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
  75. package/components/ProductPageGallery/ProductImage.graphql +1 -0
  76. package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
  77. package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
  78. package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
  79. package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
  80. package/components/ProductPagePrice/useCustomizableOptionPrice.ts +11 -53
  81. package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
  82. package/components/ProductSpecs/ProductSpecs.graphql +21 -1
  83. package/components/ProductSpecs/ProductSpecs.tsx +5 -11
  84. package/components/ProductSpecs/ProductSpecsAggregations.tsx +34 -0
  85. package/components/ProductSpecs/ProductSpecsCustomAttributes.tsx +45 -0
  86. package/components/ProductSpecs/ProductSpecsTypes.graphql +8 -0
  87. package/components/ProductStaticPaths/getProductStaticPaths.ts +3 -4
  88. package/components/ProductStaticPaths/getSitemapPaths.ts +3 -0
  89. package/components/ProductWeight/ProductWeight.tsx +12 -9
  90. package/components/index.ts +2 -0
  91. package/hooks/useProductList.ts +148 -0
  92. package/hooks/useProductListLink.ts +6 -3
  93. package/index.ts +1 -0
  94. package/package.json +14 -14
@@ -1,83 +1,124 @@
1
1
  import { LazyHydrate, RenderType, extendableComponent, responsiveVal } from '@graphcommerce/next-ui'
2
- import { Box, BoxProps } from '@mui/material'
3
- import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
2
+ import { Box, BoxProps, Breakpoint, Theme, useTheme } from '@mui/material'
4
3
  import { AddProductsToCartForm } from '../AddProductsToCart'
5
4
  import { ProductListItemProps } from '../ProductListItem/ProductListItem'
5
+ import { ProductListItemsFragment } from './ProductListItems.gql'
6
6
  import { ProductListItemRenderer } from './renderer'
7
7
 
8
8
  type ComponentState = {
9
9
  size?: 'normal' | 'small'
10
10
  }
11
11
 
12
- export type ProductItemsGridProps = {
13
- items?:
14
- | Array<(ProductListItemFragment & ProductListItemProps) | null | undefined>
15
- | null
16
- | undefined
12
+ type ColumnConfig = {
13
+ /**
14
+ * The total width of the grid, this is used to provde the correct values to the image sizes prop so the right image size is loaded.
15
+ *
16
+ * @default "calc(100vw - ${theme.page.horizontal} * 2)"
17
+ */
18
+ totalWidth?: string
19
+ /**
20
+ * Gap between the columns/rows
21
+ *
22
+ * @default theme.spacings.md
23
+ */
24
+ gap?: string
25
+ /**
26
+ * Number of columns
27
+ */
28
+ count: number
29
+ }
30
+
31
+ type ColumnsConfig = Partial<Record<Breakpoint, ColumnConfig>>
32
+
33
+ export type ProductItemsGridProps = ProductListItemsFragment & {
17
34
  renderers: ProductListItemRenderer
18
35
  loadingEager?: number
19
36
  title: string
20
37
  sx?: BoxProps['sx']
38
+ columns?: ((theme: Theme) => ColumnsConfig) | ColumnsConfig
39
+ containerRef?: React.Ref<HTMLDivElement>
21
40
  } & Pick<ProductListItemProps, 'onClick' | 'titleComponent'> &
22
41
  ComponentState
23
42
 
24
43
  const slots = ['root'] as const
25
- const name = 'ProductListItemsBase' as const
44
+ const name = 'ProductListItemsBase'
26
45
 
27
46
  const { withState } = extendableComponent<ComponentState, typeof name, typeof slots>(name, slots)
28
47
 
29
48
  export function ProductListItemsBase(props: ProductItemsGridProps) {
30
49
  const {
31
50
  items,
51
+ containerRef,
32
52
  sx = [],
33
53
  renderers,
34
54
  loadingEager = 0,
35
55
  size = 'normal',
36
56
  titleComponent,
37
57
  onClick,
58
+ columns,
38
59
  } = props
39
60
 
61
+ const theme = useTheme()
62
+
63
+ const totalWidth = `calc(100vw - ${theme.page.horizontal} * 2)`
64
+ const gap = theme.spacings.md
65
+
66
+ let columnConfig = typeof columns === 'function' ? columns(theme) : columns
67
+
68
+ if (!columnConfig && size === 'small') {
69
+ columnConfig = {
70
+ xs: { count: 2 },
71
+ md: { count: 3 },
72
+ lg: { count: 4, totalWidth: `${theme.breakpoints.values.xl}px` },
73
+ }
74
+ }
75
+
76
+ if (!columnConfig) {
77
+ columnConfig = { xs: { count: 2 }, md: { count: 3 }, lg: { count: 4 } }
78
+ }
79
+
40
80
  const classes = withState({ size })
41
81
 
42
82
  return (
43
83
  <AddProductsToCartForm>
44
84
  <Box
85
+ ref={containerRef}
45
86
  className={classes.root}
46
87
  sx={[
47
- (theme) => ({
48
- display: 'grid',
49
- gridColumnGap: theme.spacings.md,
50
- gridRowGap: theme.spacings.md,
51
-
52
- '&.sizeSmall': {
53
- gridTemplateColumns: `repeat(auto-fill, minmax(${responsiveVal(150, 280)}, 1fr))`,
54
- },
55
- '&.sizeNormal': {
56
- gridTemplateColumns: {
57
- xs: `repeat(2, 1fr)`,
58
- md: `repeat(3, 1fr)`,
59
- lg: `repeat(4, 1fr)`,
60
- },
88
+ ...Object.entries(columnConfig).map(([key, column]) => ({
89
+ [theme.breakpoints.up(key as Breakpoint)]: {
90
+ gap: column.gap ?? gap,
91
+ // width: totalWidth,
92
+ gridTemplateColumns: `repeat(${column.count}, 1fr)`,
61
93
  },
62
- }),
94
+ })),
95
+ { display: 'grid' },
63
96
  ...(Array.isArray(sx) ? sx : [sx]),
64
97
  ]}
65
98
  >
66
99
  {items?.map((item, idx) =>
67
100
  item ? (
68
- <LazyHydrate key={item.uid ?? ''} hydrated={loadingEager > idx ? true : undefined}>
101
+ <LazyHydrate
102
+ key={item.uid ?? ''}
103
+ hydrated={loadingEager > idx ? true : undefined}
104
+ height={responsiveVal(250, 500)}
105
+ >
69
106
  <RenderType
70
107
  renderer={renderers}
71
- sizes={
72
- size === 'small'
73
- ? { 0: '100vw', 354: '50vw', 675: '30vw', 1255: '23vw', 1500: '337px' }
74
- : { 0: '100vw', 367: '48vw', 994: '30vw', 1590: '23vw', 1920: '443px' }
75
- }
108
+ sizes={Object.fromEntries(
109
+ Object.entries(columnConfig ?? {}).map(([key, column]) => {
110
+ const totalW = column.totalWidth ?? totalWidth
111
+ const columnGap = column.gap ?? gap
112
+ return [
113
+ theme.breakpoints.values[key as Breakpoint],
114
+ `calc((${totalW} - (${columnGap} * ${column.count - 1})) / ${column.count})`,
115
+ ]
116
+ }),
117
+ )}
76
118
  {...item}
77
119
  loading={loadingEager > idx ? 'eager' : 'lazy'}
78
120
  titleComponent={titleComponent}
79
121
  onClick={onClick}
80
- noReport
81
122
  />
82
123
  </LazyHydrate>
83
124
  ) : null,
@@ -43,11 +43,6 @@ export function toFilterParams(params: ProductListParams): ProductFilterParams {
43
43
  }
44
44
  }
45
45
 
46
- export function toProductListParams(params: ProductFilterParams): ProductListParams {
47
- const { sort, dir, ...rest } = params
48
- return { sort: sort ? { [sort]: dir } : {}, ...rest }
49
- }
50
-
51
46
  export type AnyFilterType =
52
47
  | ProductAttributeFilterInput[keyof ProductAttributeFilterInput]
53
48
  | FilterEqualTypeInput
@@ -57,7 +52,7 @@ export type AnyFilterType =
57
52
  export function isFilterTypeEqual(filter?: unknown): filter is FilterEqualTypeInput {
58
53
  return Boolean(
59
54
  filter &&
60
- ('in' in (filter as FilterEqualTypeInput) || 'from' in (filter as FilterEqualTypeInput)),
55
+ ('in' in (filter as FilterEqualTypeInput) || 'eq' in (filter as FilterEqualTypeInput)),
61
56
  )
62
57
  }
63
58
 
@@ -72,4 +67,16 @@ export function isFilterTypeRange(filter: AnyFilterType): filter is FilterRangeT
72
67
  )
73
68
  }
74
69
 
75
- export type FilterTypes = Partial<Record<string, string>>
70
+ export function toProductListParams(params: ProductFilterParams): ProductListParams {
71
+ const { sort, dir, filters, ...rest } = params
72
+
73
+ const newFilers = Object.fromEntries(
74
+ Object.entries(filters).filter(([, value]) => {
75
+ if (isFilterTypeEqual(value)) return Boolean(value.in || value.eq)
76
+ if (isFilterTypeMatch(value)) return Boolean(value.match)
77
+ if (isFilterTypeRange(value)) return Boolean(value.from || value.to)
78
+ return false
79
+ }),
80
+ )
81
+ return { sort: sort ? { [sort]: dir } : {}, filters: newFilers, ...rest }
82
+ }
@@ -1,10 +1,13 @@
1
1
  import type {
2
2
  FilterEqualTypeInput,
3
- FilterMatchTypeInput,
4
3
  FilterRangeTypeInput,
5
4
  SortEnum,
6
5
  } from '@graphcommerce/graphql-mesh'
7
- import { FilterTypes, ProductListParams } from './filterTypes'
6
+ // eslint-disable-next-line import/no-extraneous-dependencies
7
+ import { equal } from '@wry/equality'
8
+ import { useRouter } from 'next/router'
9
+ import { ProductListParams } from './filterTypes'
10
+ import { FilterTypes } from './getFilterTypes'
8
11
 
9
12
  export function parseParams(
10
13
  url: string,
@@ -12,54 +15,58 @@ export function parseParams(
12
15
  filterTypes: FilterTypes,
13
16
  search: string | null = null,
14
17
  ): ProductListParams | undefined {
15
- const categoryVariables: ProductListParams = { url, filters: {}, sort: {}, search }
18
+ const productListParams: ProductListParams = { url, filters: {}, sort: {}, search }
16
19
 
17
20
  const typeMap = filterTypes
18
21
 
19
22
  let error = false
20
- query.reduce<string | undefined>((param, value) => {
23
+ query.map(decodeURI).reduce<string | undefined>((param, value) => {
21
24
  // We parse everything in pairs, every second loop we parse
22
25
  if (!param || param === 'q') return value
23
26
 
24
27
  if (param === 'page') {
25
- categoryVariables.currentPage = Number(value)
28
+ productListParams.currentPage = Number(value)
26
29
  return undefined
27
30
  }
28
31
  if (param === 'page-size') {
29
- categoryVariables.pageSize = Number(value)
32
+ productListParams.pageSize = Number(value)
30
33
  return undefined
31
34
  }
32
35
  if (param === 'sort') {
33
- categoryVariables.sort[value] = 'ASC'
36
+ productListParams.sort[value] = 'ASC'
34
37
  return undefined
35
38
  }
36
39
  if (param === 'dir') {
37
- const [sortBy] = Object.keys(categoryVariables.sort)
38
- if (sortBy) categoryVariables.sort[sortBy] = value?.toUpperCase() as SortEnum
40
+ const [sortBy] = Object.keys(productListParams.sort)
41
+ if (sortBy) productListParams.sort[sortBy] = value?.toUpperCase() as SortEnum
42
+ return undefined
43
+ }
44
+ if (param === 'category_uid') {
45
+ productListParams.filters.category_uid = { eq: value }
39
46
  return undefined
40
47
  }
41
48
 
42
49
  const [from, to] = value.split('-')
43
50
  switch (typeMap[param]) {
44
- case 'FilterMatchTypeInput':
45
- categoryVariables.filters[param] = { match: value } as FilterMatchTypeInput
51
+ case 'BOOLEAN':
52
+ case 'SELECT':
53
+ case 'MULTISELECT':
54
+ productListParams.filters[param] = { in: value.split(',') } as FilterEqualTypeInput
46
55
  return undefined
47
- case 'FilterRangeTypeInput':
48
- categoryVariables.filters[param] = {
56
+ case 'PRICE':
57
+ productListParams.filters[param] = {
49
58
  ...(from !== '*' && { from }),
50
59
  ...(to !== '*' && { to }),
51
60
  } as FilterRangeTypeInput
52
61
  return undefined
53
- case 'FilterEqualTypeInput':
54
- categoryVariables.filters[param] = { in: value.split(',') } as FilterEqualTypeInput
55
- return undefined
56
62
  }
57
63
 
64
+ // console.log('Filter not recognized', param, typeMap[param])
58
65
  error = true
59
66
  return undefined
60
67
  }, undefined)
61
68
 
62
- return error ? undefined : categoryVariables
69
+ return error ? undefined : productListParams
63
70
  }
64
71
 
65
72
  export function extractUrlQuery(params?: { url: string[] }) {
@@ -73,3 +80,23 @@ export function extractUrlQuery(params?: { url: string[] }) {
73
80
  if (queryIndex > 0 && !query.length) return [undefined, undefined] as const
74
81
  return [url, query] as const
75
82
  }
83
+
84
+ export function useRouterFilterParams(props: {
85
+ filterTypes?: FilterTypes | undefined
86
+ params?: ProductListParams
87
+ }) {
88
+ const { filterTypes, params } = props
89
+ const router = useRouter()
90
+
91
+ const path = router.asPath.startsWith('/c/') ? router.asPath.slice(3) : router.asPath.slice(1)
92
+ const [url, query] = extractUrlQuery({ url: path.split('#')[0].split('/') })
93
+ if (!url || !query || !filterTypes) return { params, shallow: false }
94
+
95
+ const searchParam = url.startsWith('search') ? decodeURI(url.split('/')[1] ?? '') : null
96
+ const clientParams = parseParams(url, query, filterTypes, searchParam)
97
+
98
+ if (clientParams && !clientParams?.filters.category_uid && params?.filters.category_uid)
99
+ clientParams.filters.category_uid = params?.filters.category_uid
100
+
101
+ return { params: clientParams, shallow: !equal(params, clientParams) }
102
+ }
@@ -1,5 +1,7 @@
1
1
  import { gql, ApolloClient, NormalizedCacheObject, TypedDocumentNode } from '@graphcommerce/graphql'
2
- import type { Exact } from '@graphcommerce/graphql-mesh'
2
+ import type { AttributeFrontendInputEnum, Exact } from '@graphcommerce/graphql-mesh'
3
+ import { filterNonNullableKeys, nonNullable } from '@graphcommerce/next-ui'
4
+ import { ProductFilterTypesDocument } from './ProductFilterTypes.gql'
3
5
 
4
6
  type FilterInputTypesQueryVariables = Exact<{ [key: string]: never }>
5
7
 
@@ -25,13 +27,40 @@ const FilterInputTypesDocument = gql`
25
27
  }
26
28
  ` as TypedDocumentNode<FilterInputTypesQuery, FilterInputTypesQueryVariables>
27
29
 
30
+ export type FilterTypes = Partial<Record<string, AttributeFrontendInputEnum>>
31
+
28
32
  export async function getFilterTypes(
29
33
  client: ApolloClient<NormalizedCacheObject>,
30
- ): Promise<Record<string, string | undefined>> {
34
+ isSearch: boolean = false,
35
+ ): Promise<FilterTypes> {
36
+ if (import.meta.graphCommerce.magentoVersion >= 247) {
37
+ const types = await client.query({
38
+ query: ProductFilterTypesDocument,
39
+ variables: {
40
+ filters: isSearch ? { is_filterable_in_search: true } : {},
41
+ },
42
+ })
43
+
44
+ const typeMap: FilterTypes = Object.fromEntries(
45
+ filterNonNullableKeys(types.data.attributesList?.items, ['frontend_input'])
46
+ .map((i) => [i.code, i.frontend_input])
47
+ .filter(nonNullable),
48
+ )
49
+
50
+ return typeMap
51
+ }
52
+
31
53
  const filterInputTypes = await client.query({ query: FilterInputTypesDocument })
32
54
 
33
- const typeMap: Record<string, string | undefined> = Object.fromEntries(
34
- filterInputTypes.data?.__type.inputFields.map(({ name, type }) => [name, type.name]),
55
+ const typeMap: FilterTypes = Object.fromEntries(
56
+ filterInputTypes.data?.__type.inputFields
57
+ .map<[string, AttributeFrontendInputEnum] | undefined>((field) => {
58
+ if (field.type.name === 'FilterEqualTypeInput') return [field.name, 'SELECT']
59
+ if (field.type.name === 'FilterRangeTypeInput') return [field.name, 'PRICE']
60
+ if (field.type.name === 'FilterMatchTypeInput') return [field.name, 'TEXT']
61
+ return undefined
62
+ })
63
+ .filter(nonNullable),
35
64
  )
36
65
 
37
66
  return typeMap
@@ -1,13 +1,53 @@
1
- import { StoreConfigQuery } from '@graphcommerce/magento-store'
1
+ import { cloneDeep, useQuery } from '@graphcommerce/graphql'
2
+ import { StoreConfigDocument, StoreConfigQuery } from '@graphcommerce/magento-store'
3
+ import { ProductListQueryVariables } from '../ProductList/ProductList.gql'
2
4
  import { CategoryDefaultFragment } from './CategoryDefault.gql'
3
5
  import { ProductListParams } from './filterTypes'
4
- import { cloneDeep } from '@graphcommerce/graphql'
6
+
7
+ export function useProductListApplyCategoryDefaults(
8
+ params: ProductListParams | undefined,
9
+ category: CategoryDefaultFragment | null | undefined,
10
+ ): ProductListQueryVariables | undefined {
11
+ const storeConfig = useQuery(StoreConfigDocument)
12
+
13
+ if (!params) return params
14
+
15
+ const variables = cloneDeep(params)
16
+ if (!variables.pageSize) variables.pageSize = storeConfig.data?.storeConfig?.grid_per_page ?? 12
17
+
18
+ if (Object.keys(params.sort).length === 0) {
19
+ const categorySort = category?.default_sort_by as keyof ProductListParams['sort']
20
+ const defaultSort = storeConfig.data?.storeConfig
21
+ ?.catalog_default_sort_by as keyof ProductListParams['sort']
22
+ if (categorySort) variables.sort = { [categorySort]: 'ASC' }
23
+ else if (defaultSort) variables.sort = { [defaultSort]: 'ASC' }
24
+ }
25
+
26
+ if (!variables.filters.category_uid?.in?.[0]) {
27
+ variables.filters.category_uid = { eq: category?.uid }
28
+ }
29
+
30
+ return variables
31
+ }
5
32
 
33
+ export async function productListApplyCategoryDefaults(
34
+ params: ProductListParams,
35
+ conf: StoreConfigQuery,
36
+ category:
37
+ | Promise<CategoryDefaultFragment | null | undefined>
38
+ | CategoryDefaultFragment
39
+ | null
40
+ | undefined,
41
+ ): Promise<ProductListQueryVariables>
6
42
  export async function productListApplyCategoryDefaults(
7
43
  params: ProductListParams | undefined,
8
44
  conf: StoreConfigQuery,
9
- category: Promise<CategoryDefaultFragment | null | undefined>,
10
- ) {
45
+ category:
46
+ | Promise<CategoryDefaultFragment | null | undefined>
47
+ | CategoryDefaultFragment
48
+ | null
49
+ | undefined,
50
+ ): Promise<ProductListQueryVariables | undefined> {
11
51
  if (!params) return params
12
52
 
13
53
  const newParams = cloneDeep(params)
@@ -26,3 +66,9 @@ export async function productListApplyCategoryDefaults(
26
66
 
27
67
  return newParams
28
68
  }
69
+
70
+ export function categoryDefaultsToProductListFilters(
71
+ variables: ProductListQueryVariables | undefined,
72
+ ): ProductListQueryVariables {
73
+ return { ...variables, filters: { category_uid: variables?.filters?.category_uid } }
74
+ }
@@ -1,17 +1,23 @@
1
1
  import { TypeRenderer } from '@graphcommerce/next-ui'
2
2
  import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
3
- import { ProductListItem, ProductListItemSkeleton } from '../ProductListItem/ProductListItem'
3
+ import { ProductListItem } from '../ProductListItem/ProductListItem'
4
4
 
5
5
  type SkeletonType = { __typename: 'Skeleton'; uid: string }
6
6
  export type ProductListItemType = ProductListItemFragment | SkeletonType
7
7
  export type ProductListItemRenderer = TypeRenderer<ProductListItemFragment | SkeletonType>
8
8
 
9
+ /**
10
+ * @deprecated Please use productListRenderer from the example directory instead.
11
+ */
9
12
  export const renderer: ProductListItemRenderer = {
10
- Skeleton: ProductListItemSkeleton,
13
+ Skeleton: ProductListItem,
11
14
  SimpleProduct: ProductListItem,
12
15
  ConfigurableProduct: ProductListItem,
13
16
  BundleProduct: ProductListItem,
14
17
  VirtualProduct: ProductListItem,
15
18
  DownloadableProduct: ProductListItem,
16
19
  GroupedProduct: ProductListItem,
20
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
21
+ // @ts-ignore GiftCardProduct is only available in Commerce
22
+ GiftCardProduct: ProductListItem,
17
23
  }
@@ -1,4 +1,4 @@
1
- import { Pagination } from '@graphcommerce/next-ui'
1
+ import { NextLink, PaginationExtended, Pagination } from '@graphcommerce/next-ui'
2
2
  import { Link, PaginationProps } from '@mui/material'
3
3
  import { productListLink } from '../../hooks/useProductListLink'
4
4
  import { ProductListParams } from '../ProductListItems/filterTypes'
@@ -16,23 +16,42 @@ export function ProductListPagination({
16
16
  }: ProductPaginationProps) {
17
17
  if (!page_info || !page_info.total_pages || !page_info.current_page) return null
18
18
 
19
- return (
20
- <Pagination
21
- count={page_info?.total_pages}
22
- page={page_info?.current_page ?? 1}
23
- renderLink={(_, icon, btnProps) => {
24
- const suffix = btnProps.page === 1 ? '' : `#products`
25
- return (
26
- <Link
27
- {...btnProps}
28
- href={`${productListLink({ ...params, currentPage: btnProps.page })}${suffix}`}
29
- color='inherit'
30
- >
31
- {icon}
32
- </Link>
33
- )
34
- }}
35
- {...paginationProps}
36
- />
37
- )
19
+ if (import.meta.graphCommerce.productListPaginationVariant !== 'EXTENDED') {
20
+ return (
21
+ <Pagination
22
+ count={page_info?.total_pages}
23
+ page={page_info?.current_page ?? 1}
24
+ renderLink={(_, icon, btnProps) => {
25
+ const suffix = btnProps.page === 1 ? '' : `#products`
26
+ return (
27
+ <Link
28
+ {...btnProps}
29
+ href={`${productListLink({ ...params, currentPage: btnProps.page })}${suffix}`}
30
+ component={NextLink}
31
+ shallow
32
+ color='inherit'
33
+ >
34
+ {icon}
35
+ </Link>
36
+ )
37
+ }}
38
+ {...paginationProps}
39
+ />
40
+ )
41
+ }
42
+
43
+ if (import.meta.graphCommerce.productListPaginationVariant === 'EXTENDED') {
44
+ return (
45
+ <PaginationExtended
46
+ count={page_info?.total_pages}
47
+ page={page_info?.current_page ?? 1}
48
+ paginationHref={({ page }) =>
49
+ `${productListLink({ ...params, currentPage: page })}${page === 1 ? '' : '#products'}`
50
+ }
51
+ {...paginationProps}
52
+ />
53
+ )
54
+ }
55
+
56
+ return null
38
57
  }
@@ -1,10 +1,12 @@
1
+ import { InContextMask } from '@graphcommerce/graphql'
1
2
  import { Money } from '@graphcommerce/magento-store'
2
3
  import { extendableComponent } from '@graphcommerce/next-ui'
3
- import { Typography, TypographyProps, Box } from '@mui/material'
4
+ import { Typography, TypographyProps } from '@mui/material'
4
5
  import { ProductListPriceFragment } from './ProductListPrice.gql'
5
6
 
6
7
  export const productListPrice = extendableComponent('ProductListPrice', [
7
8
  'root',
9
+ 'finalPrice',
8
10
  'discountPrice',
9
11
  ] as const)
10
12
 
@@ -18,19 +20,22 @@ export function ProductListPrice(props: ProductListPriceProps) {
18
20
  return (
19
21
  <Typography component='div' variant='body1' className={classes.root} sx={sx}>
20
22
  {regular_price.value !== final_price.value && (
21
- <Box
23
+ <InContextMask
22
24
  component='span'
23
25
  sx={{
24
26
  textDecoration: 'line-through',
25
27
  color: 'text.disabled',
26
28
  marginRight: '8px',
27
29
  }}
30
+ skeleton={{ width: '3.5em' }}
28
31
  className={classes.discountPrice}
29
32
  >
30
33
  <Money {...regular_price} />
31
- </Box>
34
+ </InContextMask>
32
35
  )}
33
- <Money {...final_price} />
36
+ <InContextMask className={classes.finalPrice} component='span' skeleton={{ width: '3.5em' }}>
37
+ <Money {...final_price} />
38
+ </InContextMask>
34
39
  </Typography>
35
40
  )
36
41
  }
@@ -0,0 +1,5 @@
1
+ fragment ProductListSuggestions on Products {
2
+ suggestions {
3
+ search
4
+ }
5
+ }
@@ -0,0 +1,42 @@
1
+ import { ListFormat, filterNonNullableKeys } from '@graphcommerce/next-ui'
2
+ import { Trans } from '@lingui/macro'
3
+ import { Box, Link } from '@mui/material'
4
+ import { productListLinkFromFilter } from '../../hooks/useProductListLink'
5
+ import { useProductFiltersPro } from '../ProductFiltersPro'
6
+ import { ProductListSuggestionsFragment } from './ProductListSuggestions.gql'
7
+
8
+ type ProductListSuggestionsProps = {
9
+ products: ProductListSuggestionsFragment
10
+ }
11
+
12
+ export function ProductListSuggestions(props: ProductListSuggestionsProps) {
13
+ const { products } = props
14
+
15
+ const { form, submit, params } = useProductFiltersPro()
16
+
17
+ if (!products.suggestions || !products.suggestions.length) return null
18
+
19
+ const list = (
20
+ <ListFormat listStyle='short' type='disjunction'>
21
+ {filterNonNullableKeys(products.suggestions).map((s) => (
22
+ <Link
23
+ key={s.search}
24
+ href={productListLinkFromFilter({ ...params, search: s.search })}
25
+ onClick={() => {
26
+ form.setValue('currentPage', 1)
27
+ form.setValue('search', s.search)
28
+ return submit()
29
+ }}
30
+ >
31
+ {s.search}
32
+ </Link>
33
+ ))}
34
+ </ListFormat>
35
+ )
36
+
37
+ return (
38
+ <Box>
39
+ <Trans>Did you mean: {list}</Trans>
40
+ </Box>
41
+ )
42
+ }
@@ -1,5 +1,8 @@
1
1
  fragment ProductPageBreadcrumb on ProductInterface {
2
+ __typename
3
+ uid
2
4
  name
5
+ url_key
3
6
  categories {
4
7
  uid
5
8
  name
@@ -8,6 +8,9 @@ import { ProductPageBreadcrumbFragment } from './ProductPageBreadcrumb.gql'
8
8
  type ProductPageBreadcrumbsProps = ProductPageBreadcrumbFragment &
9
9
  Omit<BreadcrumbsProps, 'children'>
10
10
 
11
+ /**
12
+ * @deprecated Please use ProductPageBreadcrumbs
13
+ */
11
14
  export function ProductPageBreadcrumb(props: ProductPageBreadcrumbsProps) {
12
15
  const { categories, name, ...breadcrumbProps } = props
13
16
  const prev = usePrevPageRouter()