@graphcommerce/magento-product 8.1.0-canary.9 → 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 (93) hide show
  1. package/Api/ProductListItem.graphql +1 -2
  2. package/Api/ProductPageItem.graphql +1 -1
  3. package/CHANGELOG.md +276 -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 +1 -1
  88. package/components/ProductWeight/ProductWeight.tsx +12 -9
  89. package/components/index.ts +2 -0
  90. package/hooks/useProductList.ts +148 -0
  91. package/hooks/useProductListLink.ts +6 -3
  92. package/index.ts +1 -0
  93. package/package.json +14 -14
@@ -1,9 +1,25 @@
1
- import { useForm, UseFormProps, UseFormReturn } from '@graphcommerce/ecommerce-ui'
1
+ import {
2
+ DeepPartial,
3
+ FormAutoSubmit,
4
+ useForm,
5
+ UseFormProps,
6
+ UseFormReturn,
7
+ WatchObserver,
8
+ } from '@graphcommerce/ecommerce-ui'
2
9
  import { useMatchMediaMotionValue, useMemoObject } from '@graphcommerce/next-ui'
3
- import { useEventCallback, useTheme } from '@mui/material'
10
+ import { Theme, useEventCallback, useMediaQuery, useTheme } from '@mui/material'
4
11
  import { m, useTransform } from 'framer-motion'
5
12
  import { useRouter } from 'next/router'
6
- import React, { BaseSyntheticEvent, createContext, useContext, useMemo, useRef } from 'react'
13
+ import React, {
14
+ BaseSyntheticEvent,
15
+ createContext,
16
+ MutableRefObject,
17
+ useContext,
18
+ useEffect,
19
+ useMemo,
20
+ useRef,
21
+ } from 'react'
22
+ import type { Subscription } from 'react-hook-form/dist/utils/createSubject'
7
23
  import { productListLinkFromFilter } from '../../hooks/useProductListLink'
8
24
  import { ProductListFiltersFragment } from '../ProductListFilters/ProductListFilters.gql'
9
25
  import {
@@ -17,23 +33,40 @@ type DataProps = {
17
33
  appliedAggregations?: ProductListFiltersFragment['aggregations']
18
34
  } & ProductListFiltersFragment
19
35
 
20
- type FilterFormContextProps = DataProps & {
36
+ export type ProductFiltersProContext = DataProps & {
21
37
  /**
22
38
  * Watch and formState are known to cause performance issues.
23
39
  *
24
40
  * - `watch` -> `useWatch`
25
41
  * - `formState` -> `useFormState`
26
42
  */
27
- form: Omit<UseFormReturn<ProductFilterParams>, 'formState' | 'watch'>
43
+ form: Omit<UseFormReturn<ProductFilterParams>, 'formState' | 'watch'> & {
44
+ watch: (
45
+ callback: WatchObserver<ProductFilterParams>,
46
+ defaultValues?: DeepPartial<ProductFilterParams>,
47
+ ) => Subscription
48
+ }
49
+ /**
50
+ * Parameters of the currently displayed items.
51
+ *
52
+ * To get active form values use `useWatch`.
53
+ */
28
54
  params: ProductFilterParams
29
55
  submit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>
30
56
  }
31
57
 
32
- const FilterFormContext = createContext<FilterFormContextProps | null>(null)
58
+ const FilterFormContext = createContext<ProductFiltersProContext | null>(null)
33
59
 
34
- export const useProductFiltersPro = () => {
60
+ export const globalFormContextRef: MutableRefObject<ProductFiltersProContext | null> = {
61
+ current: null,
62
+ }
63
+
64
+ export function useProductFiltersPro(optional: true): ProductFiltersProContext | null
65
+ export function useProductFiltersPro(optional?: false): ProductFiltersProContext
66
+ export function useProductFiltersPro(optional: boolean = false) {
35
67
  const context = useContext(FilterFormContext)
36
- if (!context) throw Error('useProductFiltersPro should be used inside ProductFiltersPro')
68
+ if (!optional && !context)
69
+ throw Error('useProductFiltersPro should be used inside ProductFiltersPro')
37
70
  return context
38
71
  }
39
72
 
@@ -43,12 +76,47 @@ export type FilterFormProviderProps = Omit<
43
76
  > & {
44
77
  children: React.ReactNode
45
78
  params: ProductListParams
79
+ /**
80
+ * Whether the filter should scroll to the products list and whether to submit the form on change.
81
+ */
82
+ autoSubmitMd?: boolean
83
+
84
+ handleSubmit?: (
85
+ formValues: ProductFilterParams,
86
+ next: (shallow?: boolean, replace?: boolean) => Promise<void>,
87
+ ) => Promise<void> | void
46
88
  } & DataProps
47
89
 
48
- const isSidebar = import.meta.graphCommerce.productFiltersLayout === 'SIDEBAR'
90
+ function AutoSubmitSidebarDesktop() {
91
+ const { form, submit } = useProductFiltersPro()
92
+
93
+ // We only need to auto-submit when the layout is not sidebar and we're viewing on desktop
94
+ const autoSubmitDisabled = useMediaQuery<Theme>((t) => t.breakpoints.down('md'), {
95
+ defaultMatches: false,
96
+ })
97
+
98
+ return (
99
+ <FormAutoSubmit
100
+ control={form.control}
101
+ disabled={autoSubmitDisabled}
102
+ submit={submit}
103
+ leading
104
+ name={['filters', 'url', 'sort', 'pageSize', 'currentPage', 'dir']}
105
+ />
106
+ )
107
+ }
49
108
 
50
109
  export function ProductFiltersPro(props: FilterFormProviderProps) {
51
- const { children, params, aggregations, appliedAggregations, filterTypes, ...formProps } = props
110
+ const {
111
+ children,
112
+ params,
113
+ aggregations,
114
+ appliedAggregations,
115
+ filterTypes,
116
+ autoSubmitMd = false,
117
+ handleSubmit,
118
+ ...formProps
119
+ } = props
52
120
 
53
121
  const defaultValues = useMemoObject(toFilterParams(params))
54
122
  const form = useForm<ProductFilterParams>({ defaultValues, ...formProps })
@@ -58,36 +126,52 @@ export function ProductFiltersPro(props: FilterFormProviderProps) {
58
126
  const theme = useTheme()
59
127
  const isDesktop = useMatchMediaMotionValue('up', 'md')
60
128
  const scrollMarginTop = useTransform(() => (isDesktop.get() ? 0 : theme.appShell.headerHeightSm))
61
- const scroll = useTransform(() => !isSidebar || isDesktop.get())
129
+ const scroll = useTransform(() => !autoSubmitMd || isDesktop.get())
62
130
 
63
131
  const submit = useEventCallback(
64
132
  form.handleSubmit(async (formValues) => {
65
133
  const path = productListLinkFromFilter({ ...formValues, currentPage: 1 })
66
134
  if (router.asPath === path) return false
67
135
 
68
- const opts = { scroll: scroll.get() }
69
- return (router.query.url ?? []).includes('q')
70
- ? router.replace(path, path, opts)
71
- : router.push(path, path, opts)
136
+ const isSearch = router.asPath.startsWith('/search')
137
+ const isFilter = (router.query.url ?? []).includes('q')
138
+
139
+ const next = async (shallow = false, replace: boolean = isSearch || isFilter) => {
140
+ const opts = { shallow, scroll: scroll.get() }
141
+ await (replace ? router.replace(path, path, opts) : router.push(path, path, opts))
142
+ }
143
+
144
+ if (handleSubmit) return handleSubmit(formValues, next)
145
+ return next()
72
146
  }),
73
147
  )
74
148
 
75
- const filterFormContext: FilterFormContextProps = useMemo(
76
- () => ({
149
+ const filterFormContext = useMemo(() => {
150
+ const ctx: ProductFiltersProContext = {
77
151
  form,
78
152
  params: defaultValues,
79
153
  submit,
80
154
  appliedAggregations,
81
155
  filterTypes,
82
156
  aggregations,
83
- }),
84
- [form, defaultValues, submit, appliedAggregations, filterTypes, aggregations],
157
+ }
158
+ globalFormContextRef.current = ctx
159
+ return ctx
160
+ }, [form, defaultValues, submit, appliedAggregations, filterTypes, aggregations])
161
+
162
+ // When the component unmounts, we want to clear the global filter form
163
+ useEffect(
164
+ () => () => {
165
+ globalFormContextRef.current = null
166
+ },
167
+ [],
85
168
  )
86
169
 
87
170
  return (
88
171
  <FilterFormContext.Provider value={filterFormContext}>
89
172
  <m.form ref={ref} noValidate onSubmit={submit} id='products' style={{ scrollMarginTop }} />
90
173
  {children}
174
+ {autoSubmitMd && <AutoSubmitSidebarDesktop />}
91
175
  </FilterFormContext.Provider>
92
176
  )
93
177
  }
@@ -1,4 +1,9 @@
1
+ import { AttributeFrontendInputEnum } from '@graphcommerce/graphql-mesh'
1
2
  import { ProductListFiltersFragment } from '../ProductListFilters/ProductListFilters.gql'
3
+ import { ProductFilterEqualChip } from './ProductFilterEqualChip'
4
+ import { ProductFilterEqualSection } from './ProductFilterEqualSection'
5
+ import { ProductFilterRangeChip } from './ProductFilterRangeChip'
6
+ import { ProductFilterRangeSection } from './ProductFilterRangeSection'
2
7
  import { useProductFiltersPro } from './ProductFiltersPro'
3
8
  import { excludeCategory } from './activeAggregations'
4
9
  import { applyAggregationCount } from './applyAggregationCount'
@@ -7,10 +12,24 @@ export type FilterProps = {
7
12
  aggregation: NonNullable<NonNullable<ProductListFiltersFragment['aggregations']>[number]>
8
13
  }
9
14
 
10
- export type FilterRenderer = Record<string, React.FC<FilterProps>>
15
+ export type FilterRenderer = Record<AttributeFrontendInputEnum, React.FC<FilterProps>>
11
16
 
12
17
  export type ProductFiltersProAggregationsProps = {
13
- renderer?: FilterRenderer
18
+ renderer?: Partial<FilterRenderer>
19
+ }
20
+
21
+ export const productFiltersProSectionRenderer: Partial<FilterRenderer> = {
22
+ SELECT: ProductFilterEqualSection,
23
+ MULTISELECT: ProductFilterEqualSection,
24
+ BOOLEAN: ProductFilterEqualSection,
25
+ PRICE: ProductFilterRangeSection,
26
+ }
27
+
28
+ export const productFiltersProChipRenderer: Partial<FilterRenderer> = {
29
+ SELECT: ProductFilterEqualChip,
30
+ MULTISELECT: ProductFilterEqualChip,
31
+ BOOLEAN: ProductFilterEqualChip,
32
+ PRICE: ProductFilterRangeChip,
14
33
  }
15
34
 
16
35
  export function ProductFiltersProAggregations(props: ProductFiltersProAggregationsProps) {
@@ -19,26 +38,28 @@ export function ProductFiltersProAggregations(props: ProductFiltersProAggregatio
19
38
 
20
39
  return (
21
40
  <>
22
- {excludeCategory(
23
- applyAggregationCount(aggregations, appliedAggregations, params),
24
- params,
25
- ).map((aggregation) => {
26
- const filterType = filterTypes[aggregation.attribute_code]
27
- if (!filterType) return null
28
-
29
- const Component = renderer?.[filterType]
30
- if (!Component) {
31
- if (process.env.NODE_ENV === 'development') {
32
- // eslint-disable-next-line no-console
33
- console.log(
34
- `The renderer for filterType ${filterType} can not be found, please add it to the renderer prop: renderer={{ ${filterType}: (props) => <>MYRenderer</> }}}}`,
35
- )
41
+ {excludeCategory(applyAggregationCount(aggregations, appliedAggregations, params)).map(
42
+ (aggregation) => {
43
+ const filterType = filterTypes[aggregation.attribute_code]
44
+ if (!filterType) {
45
+ // console.log('Filter not recognized', aggregation.attribute_code, filterTypes)
46
+ return null
47
+ }
48
+
49
+ const Component = renderer?.[filterType]
50
+ if (!Component) {
51
+ if (process.env.NODE_ENV === 'development') {
52
+ // eslint-disable-next-line no-console
53
+ console.log(
54
+ `The renderer for filterType ${filterType} can not be found, please add it to the renderer prop: renderer={{ ${filterType}: (props) => <>MYRenderer</> }}}}`,
55
+ )
56
+ }
57
+ return null
36
58
  }
37
- return null
38
- }
39
59
 
40
- return <Component key={aggregation.attribute_code} aggregation={aggregation} {...props} />
41
- })}
60
+ return <Component key={aggregation.attribute_code} aggregation={aggregation} {...props} />
61
+ },
62
+ )}
42
63
  </>
43
64
  )
44
65
  }
@@ -1,11 +1,10 @@
1
1
  import { ChipOverlayOrPopper, ChipOverlayOrPopperProps } from '@graphcommerce/next-ui'
2
2
  import { Trans } from '@lingui/react'
3
- import { ProductFilterEqualSection } from './ProductFilterEqualSection'
4
- import { ProductFilterRangeSection } from './ProductFilterRangeSection'
5
3
  import { useProductFiltersPro } from './ProductFiltersPro'
6
4
  import {
7
5
  ProductFiltersProAggregations,
8
6
  ProductFiltersProAggregationsProps,
7
+ productFiltersProSectionRenderer,
9
8
  } from './ProductFiltersProAggregations'
10
9
  import { ProductFiltersProLimitSection } from './ProductFiltersProLimitSection'
11
10
  import {
@@ -14,7 +13,7 @@ import {
14
13
  } from './ProductFiltersProSortSection'
15
14
  import { activeAggregations } from './activeAggregations'
16
15
  import { applyAggregationCount } from './applyAggregationCount'
17
- import { useClearAllFiltersAction } from './useClearAllFiltersHandler'
16
+ import { useProductFiltersProClearAllAction } from './useProductFiltersProClearAllAction'
18
17
 
19
18
  export type ProductFiltersProAllFiltersChipProps = ProductFiltersProAggregationsProps &
20
19
  ProductFiltersProSortSectionProps &
@@ -23,11 +22,6 @@ export type ProductFiltersProAllFiltersChipProps = ProductFiltersProAggregations
23
22
  'label' | 'selected' | 'selectedLabel' | 'onApply' | 'onReset' | 'onClose' | 'children'
24
23
  >
25
24
 
26
- const defaultRenderer = {
27
- FilterRangeTypeInput: ProductFilterRangeSection,
28
- FilterEqualTypeInput: ProductFilterEqualSection,
29
- }
30
-
31
25
  export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFiltersChipProps) {
32
26
  const { sort_fields, total_count, renderer, category, ...rest } = props
33
27
 
@@ -42,7 +36,7 @@ export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFilte
42
36
  const allFilters = [...activeFilters, sort].filter(Boolean)
43
37
  const hasFilters = allFilters.length > 0
44
38
 
45
- const clearAll = useClearAllFiltersAction()
39
+ const clearAll = useProductFiltersProClearAllAction()
46
40
 
47
41
  return (
48
42
  <ChipOverlayOrPopper
@@ -65,7 +59,9 @@ export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFilte
65
59
  category={category}
66
60
  />
67
61
  <ProductFiltersProLimitSection />
68
- <ProductFiltersProAggregations renderer={{ ...defaultRenderer, ...renderer }} />
62
+ <ProductFiltersProAggregations
63
+ renderer={{ ...productFiltersProSectionRenderer, ...renderer }}
64
+ />
69
65
  </>
70
66
  )}
71
67
  </ChipOverlayOrPopper>
@@ -4,7 +4,12 @@ import { ProductFilterRangeSection } from './ProductFilterRangeSection'
4
4
  import {
5
5
  ProductFiltersProAggregations,
6
6
  ProductFiltersProAggregationsProps,
7
+ productFiltersProSectionRenderer,
7
8
  } from './ProductFiltersProAggregations'
9
+ import {
10
+ ProductFiltersCategorySectionProps,
11
+ ProductFiltersProCategorySection,
12
+ } from './ProductFiltersProCategorySection'
8
13
  import { ProductFiltersProLimitSection } from './ProductFiltersProLimitSection'
9
14
  import {
10
15
  ProductFiltersProSortSection,
@@ -12,25 +17,30 @@ import {
12
17
  } from './ProductFiltersProSortSection'
13
18
 
14
19
  export type ProductFiltersProAllFiltersSidebarProps = ProductFiltersProAggregationsProps &
15
- ProductFiltersProSortSectionProps & { sx?: SxProps<Theme> }
16
-
17
- const defaultRenderer = {
18
- FilterRangeTypeInput: ProductFilterRangeSection,
19
- FilterEqualTypeInput: ProductFilterEqualSection,
20
- }
20
+ ProductFiltersProSortSectionProps &
21
+ ProductFiltersCategorySectionProps & { sx?: SxProps<Theme> }
21
22
 
23
+ /**
24
+ * @deprecated Not used anymore
25
+ *
26
+ * @param props
27
+ * @returns
28
+ */
22
29
  export function ProductFiltersProAllFiltersSidebar(props: ProductFiltersProAllFiltersSidebarProps) {
23
- const { sort_fields, total_count, renderer, sx = [], category } = props
30
+ const { sort_fields, total_count, renderer, sx = [], category, params } = props
24
31
 
25
32
  return (
26
33
  <Box sx={[{ display: { xs: 'none', md: 'grid' } }, ...(Array.isArray(sx) ? sx : [sx])]}>
34
+ <ProductFiltersProCategorySection category={category} params={params} />
27
35
  <ProductFiltersProSortSection
28
36
  sort_fields={sort_fields}
29
37
  total_count={total_count}
30
38
  category={category}
31
39
  />
32
40
  <ProductFiltersProLimitSection />
33
- <ProductFiltersProAggregations renderer={{ ...defaultRenderer, ...renderer }} />
41
+ <ProductFiltersProAggregations
42
+ renderer={{ ...productFiltersProSectionRenderer, ...renderer }}
43
+ />
34
44
  </Box>
35
45
  )
36
46
  }
@@ -0,0 +1,130 @@
1
+ import { useWatch } from '@graphcommerce/ecommerce-ui'
2
+ import {
3
+ CategoryTreeItem,
4
+ UseCategoryTreeProps,
5
+ useCategoryTree,
6
+ } from '@graphcommerce/magento-category'
7
+ import {
8
+ ActionCard,
9
+ ActionCardAccordion,
10
+ ActionCardAccordionProps,
11
+ ActionCardList,
12
+ Button,
13
+ IconSvg,
14
+ iconChevronLeft,
15
+ responsiveVal,
16
+ } from '@graphcommerce/next-ui'
17
+ import { Trans } from '@lingui/react'
18
+ import { Box, SxProps, Theme } from '@mui/material'
19
+ import { useProductFiltersPro } from './ProductFiltersPro'
20
+
21
+ export type ProductFiltersProCategoryAccordionProps = {
22
+ hideTitle?: boolean
23
+ sx?: SxProps<Theme>
24
+ categoryTree: CategoryTreeItem[]
25
+ onChange: (uid: CategoryTreeItem) => void | Promise<void>
26
+ } & Pick<ActionCardAccordionProps, 'defaultExpanded'>
27
+
28
+ export function ProductFiltersProCategoryAccordion(props: ProductFiltersProCategoryAccordionProps) {
29
+ const { hideTitle, sx, categoryTree, onChange, defaultExpanded } = props
30
+ const { form } = useProductFiltersPro()
31
+
32
+ const name = `filters.category_uid.in` as const
33
+ const currentFilter = useWatch({ control: form.control, name })
34
+
35
+ return (
36
+ <ActionCardAccordion
37
+ sx={[
38
+ hideTitle ? { '& .MuiAccordionSummary-root': { display: 'none' } } : {},
39
+ sx,
40
+ ...(Array.isArray(sx) ? sx : [sx]),
41
+ ]}
42
+ defaultExpanded={defaultExpanded}
43
+ summary={<Trans id='Categories' />}
44
+ right={
45
+ currentFilter && currentFilter.length > 0 ? (
46
+ <Button
47
+ color='primary'
48
+ onClick={(e) => {
49
+ e.stopPropagation()
50
+ form.setValue(name, null)
51
+ }}
52
+ >
53
+ <Trans id='Clear' />
54
+ </Button>
55
+ ) : undefined
56
+ }
57
+ details={
58
+ <ActionCardList
59
+ size='responsive'
60
+ variant='default'
61
+ sx={{ mb: 2 }}
62
+ value={form.getValues('url')}
63
+ onChange={async (e, value) => {
64
+ const item = categoryTree.find((i) => i.value === value)
65
+ if (!item) return
66
+ await onChange(item)
67
+ }}
68
+ >
69
+ {categoryTree.map((item) => {
70
+ const indent = item.isBack ? 0 : item.indent + 1
71
+ return (
72
+ <ActionCard
73
+ key={item.value}
74
+ {...item}
75
+ size='responsive'
76
+ title={
77
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
78
+ <Box sx={{ marginRight: 1 }}>
79
+ {item.isBack ? (
80
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
81
+ <IconSvg src={iconChevronLeft} size='medium' />
82
+ {item.title}
83
+ </Box>
84
+ ) : (
85
+ item.title
86
+ )}
87
+ </Box>
88
+ {item.count !== null && (
89
+ <Box sx={{ typography: 'caption', color: 'text.disabled' }}>
90
+ ({item.count})
91
+ </Box>
92
+ )}
93
+ </Box>
94
+ }
95
+ sx={{
96
+ '&.sizeSmall': { pl: responsiveVal(8 * indent, 12 * indent) },
97
+ '&.sizeMedium': { pl: responsiveVal(10 * indent, 14 * indent) },
98
+ '&.sizeLarge': { pl: responsiveVal(12 * indent, 16 * indent) },
99
+ '&.sizeResponsive': { pl: responsiveVal(8 * indent, 16 * indent) },
100
+ '& .ActionCard-title.selected': { fontWeight: 'bold' },
101
+ }}
102
+ />
103
+ )
104
+ })}
105
+ </ActionCardList>
106
+ }
107
+ />
108
+ )
109
+ }
110
+
111
+ export type ProductFiltersCategorySectionProps = UseCategoryTreeProps &
112
+ Omit<ProductFiltersProCategoryAccordionProps, 'categoryTree' | 'onChange'>
113
+
114
+ export function ProductFiltersProCategorySection(props: ProductFiltersCategorySectionProps) {
115
+ const categoryTree = useCategoryTree(props)
116
+ const { form, submit } = useProductFiltersPro()
117
+
118
+ if (!categoryTree) return null
119
+ return (
120
+ <ProductFiltersProCategoryAccordion
121
+ categoryTree={categoryTree}
122
+ {...props}
123
+ onChange={async (item) => {
124
+ form.setValue('url', item.value)
125
+ form.setValue('filters', { category_uid: { in: [item?.uid] } })
126
+ await submit()
127
+ }}
128
+ />
129
+ )
130
+ }
@@ -1,16 +1,18 @@
1
- import { ProductFilterEqualChip } from './ProductFilterEqualChip'
2
- import { ProductFilterRangeChip } from './ProductFilterRangeChip'
3
1
  import {
4
2
  ProductFiltersProAggregations,
5
3
  ProductFiltersProAggregationsProps,
4
+ productFiltersProChipRenderer,
6
5
  } from './ProductFiltersProAggregations'
7
6
 
8
- const defaultRenderer = {
9
- FilterEqualTypeInput: ProductFilterEqualChip,
10
- FilterRangeTypeInput: ProductFilterRangeChip,
11
- }
12
-
7
+ /**
8
+ * @deprecated Not used anymore, use `<ProductFiltersProAggregations renderer={productFiltersProChipRenderer}/>`
9
+ */
13
10
  export function ProductFiltersProFilterChips(props: ProductFiltersProAggregationsProps) {
14
11
  const { renderer } = props
15
- return <ProductFiltersProAggregations {...props} renderer={{ ...defaultRenderer, ...renderer }} />
12
+ return (
13
+ <ProductFiltersProAggregations
14
+ {...props}
15
+ renderer={{ ...productFiltersProChipRenderer, ...renderer }}
16
+ />
17
+ )
16
18
  }
@@ -1,11 +1,9 @@
1
1
  import { Button } from '@graphcommerce/next-ui'
2
2
  import { Trans } from '@lingui/react'
3
3
  import { SxProps, Theme } from '@mui/material'
4
- import { useProductFiltersPro } from './ProductFiltersPro'
5
4
  import { ProductFiltersProAggregationsProps } from './ProductFiltersProAggregations'
6
- import { activeAggregations } from './activeAggregations'
7
- import { applyAggregationCount } from './applyAggregationCount'
8
- import { useClearAllFiltersAction } from './useClearAllFiltersHandler'
5
+ import { useProductFiltersProClearAllAction } from './useProductFiltersProClearAllAction'
6
+ import { useProductFilterProHasFiltersApplied } from './useProductFiltersProHasFiltersApplied'
9
7
 
10
8
  type AllFiltersSidebar = ProductFiltersProAggregationsProps & {
11
9
  sx?: SxProps<Theme>
@@ -14,18 +12,8 @@ type AllFiltersSidebar = ProductFiltersProAggregationsProps & {
14
12
  export function ProductFiltersProClearAll(props: AllFiltersSidebar) {
15
13
  const { sx = [] } = props
16
14
 
17
- const { params, aggregations, appliedAggregations } = useProductFiltersPro()
18
- const { sort } = params
19
-
20
- const clearAll = useClearAllFiltersAction()
21
-
22
- const activeFilters = activeAggregations(
23
- applyAggregationCount(aggregations, appliedAggregations, params),
24
- params,
25
- ).map(({ label }) => label)
26
-
27
- const allFilters = [...activeFilters, sort].filter(Boolean)
28
- const hasFilters = allFilters.length > 0
15
+ const clearAll = useProductFiltersProClearAllAction()
16
+ const hasFilters = useProductFilterProHasFiltersApplied()
29
17
 
30
18
  if (!hasFilters) return null
31
19
 
@@ -12,6 +12,7 @@ export type ProductFiltersProLayoutSidebarProps = {
12
12
  count?: React.ReactNode
13
13
  pagination: React.ReactNode
14
14
  header?: React.ReactNode
15
+ children?: React.ReactNode
15
16
  } & Partial<OwnerProps>
16
17
 
17
18
  type OwnerProps = {
@@ -32,6 +33,7 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
32
33
  sidebarFilters,
33
34
  header,
34
35
  headerPosition = 'before',
36
+ children,
35
37
  } = props
36
38
 
37
39
  const { form, submit } = useProductFiltersPro()
@@ -47,8 +49,6 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
47
49
 
48
50
  <FormAutoSubmit control={form.control} disabled={autoSubmitDisabled} submit={submit} />
49
51
 
50
- <StickyBelowHeader sx={{ display: { md: 'none' } }}>{horizontalFilters}</StickyBelowHeader>
51
-
52
52
  <Container
53
53
  maxWidth={false}
54
54
  className={classes.content}
@@ -56,12 +56,16 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
56
56
  display: 'grid',
57
57
  gridTemplate: {
58
58
  xs: `
59
- "beforeContent" auto
60
- "items" auto
61
- "afterContent" auto
59
+ "content" auto
60
+ "horizontalFilters" auto
61
+ "beforeContent" auto
62
+ "items" auto
63
+ "afterContent" auto
62
64
  `,
63
- md: `
64
- "topleft beforeContent" auto
65
+ md: `
66
+ "topleft content" auto
67
+ "sidebar content" auto
68
+ "sidebar beforeContent" auto
65
69
  "sidebar items" min-content
66
70
  "sidebar afterContent" 1fr
67
71
  /300px auto
@@ -87,6 +91,10 @@ export function ProductFiltersProLayoutSidebar(props: ProductFiltersProLayoutSid
87
91
  {sidebarFilters}
88
92
  </Box>
89
93
  )}
94
+ {children && <Box gridArea='content'>{children}</Box>}
95
+ <StickyBelowHeader sx={{ display: { md: 'none', gridArea: 'horizontalFilters' } }}>
96
+ {horizontalFilters}
97
+ </StickyBelowHeader>
90
98
 
91
99
  <Box gridArea='beforeContent' sx={{ mt: { md: 0 } }}>
92
100
  {count}
@@ -1,13 +1,7 @@
1
- import { useWatch } from '@graphcommerce/ecommerce-ui'
1
+ import { ActionCardItemBase, ActionCardListForm, useWatch } from '@graphcommerce/ecommerce-ui'
2
2
  import { useQuery } from '@graphcommerce/graphql'
3
3
  import { StoreConfigDocument } from '@graphcommerce/magento-store'
4
- import {
5
- ActionCard,
6
- ActionCardItemBase,
7
- ActionCardListForm,
8
- ChipOverlayOrPopper,
9
- ChipOverlayOrPopperProps,
10
- } from '@graphcommerce/next-ui'
4
+ import { ActionCard, ChipOverlayOrPopper, ChipOverlayOrPopperProps } from '@graphcommerce/next-ui'
11
5
  import { Trans } from '@lingui/react'
12
6
  import { useMemo } from 'react'
13
7
  import { useProductFiltersPro } from './ProductFiltersPro'