@graphcommerce/magento-product 8.1.0-canary.8 → 9.0.0-canary.54
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.
- package/Api/ProductListItem.graphql +1 -2
- package/Api/ProductPageItem.graphql +1 -1
- package/CHANGELOG.md +113 -0
- package/Config.graphqls +13 -0
- package/components/AddProductsToCart/AddProductsToCartButton.tsx +3 -2
- package/components/AddProductsToCart/AddProductsToCartFab.tsx +2 -2
- package/components/AddProductsToCart/AddProductsToCartForm.tsx +31 -28
- package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +25 -16
- package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
- package/components/AddProductsToCart/findAddedItems.ts +1 -4
- package/components/AddProductsToCart/useAddProductsToCartAction.ts +2 -1
- package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
- package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
- package/components/JsonLdProduct/ProductPageJsonLd.tsx +1 -1
- package/components/ProductAddToCart/ProductAddToCart.tsx +6 -8
- package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
- package/components/ProductCustomizable/index.ts +1 -0
- package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
- package/components/ProductFiltersPro/PriceSlider.tsx +1 -2
- package/components/ProductFiltersPro/ProductFilterEqualChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFilterEqualSection.tsx +2 -2
- package/components/ProductFiltersPro/ProductFilterRangeChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFilterRangeSection.tsx +1 -1
- package/components/ProductFiltersPro/ProductFiltersPro.tsx +103 -19
- package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +31 -18
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -10
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +18 -8
- package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +130 -0
- package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
- package/components/ProductFiltersPro/ProductFiltersProClearAll.tsx +4 -16
- package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
- package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +5 -2
- package/components/ProductFiltersPro/ProductFiltersProNoResults.tsx +79 -0
- package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +1 -1
- package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +2 -4
- package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +7 -2
- package/components/ProductFiltersPro/activeAggregations.ts +5 -9
- package/components/ProductFiltersPro/applyAggregationCount.ts +14 -8
- package/components/ProductFiltersPro/index.ts +9 -0
- package/components/ProductFiltersPro/{useClearAllFiltersHandler.ts → useProductFiltersProClearAllAction.ts} +1 -1
- package/components/ProductFiltersPro/useProductFiltersProHasFiltersApplied.ts +21 -0
- package/components/ProductFiltersPro/useProductFiltersProSort.tsx +4 -2
- package/components/ProductList/ProductList.graphql +8 -5
- package/components/ProductListCount/ProductListCount.tsx +3 -1
- package/components/ProductListFilters/ProductFilters.graphql +7 -2
- package/components/ProductListFilters/ProductListFilters.graphql +1 -1
- package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
- package/components/ProductListItem/ProductDiscountLabel.tsx +2 -3
- package/components/ProductListItem/ProductListItem.tsx +3 -3
- package/components/ProductListItem/ProductListItemTitleAndPrice.tsx +18 -15
- package/components/ProductListItems/ProductListItemsBase.tsx +65 -23
- package/components/ProductListItems/filterTypes.tsx +14 -5
- package/components/ProductListItems/filteredProductList.tsx +23 -0
- package/components/ProductListItems/productListApplyCategoryDefaults.ts +44 -4
- package/components/ProductListItems/renderer.tsx +8 -2
- package/components/ProductListPagination/ProductListPagination.tsx +39 -20
- package/components/ProductListPrice/ProductListPrice.tsx +9 -4
- package/components/ProductListSuggestions/ProductListSuggestions.graphql +5 -0
- package/components/ProductListSuggestions/ProductListSuggestions.tsx +42 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +3 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
- package/components/ProductPageBreadcrumb/index.ts +1 -0
- package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
- package/components/ProductPageDescription/ProductPageDescription.tsx +1 -1
- package/components/ProductPageGallery/ProductImage.graphql +1 -0
- package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
- package/components/ProductPagePrice/ProductPagePrice.graphql +0 -6
- package/components/ProductPagePrice/ProductPagePrice.tsx +19 -12
- package/components/ProductPagePrice/ProductPagePriceTiers.tsx +4 -3
- package/components/ProductPagePrice/useCustomizableOptionPrice.ts +11 -53
- package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
- package/components/ProductStaticPaths/getProductStaticPaths.ts +2 -3
- package/components/ProductStaticPaths/getSitemapPaths.ts +3 -0
- package/components/ProductWeight/ProductWeight.tsx +12 -9
- package/components/index.ts +2 -0
- package/hooks/useProductList.ts +123 -0
- package/hooks/useProductListLink.ts +6 -3
- package/index.ts +1 -0
- package/package.json +14 -13
@@ -1,9 +1,25 @@
|
|
1
|
-
import {
|
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, {
|
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
|
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<
|
58
|
+
const FilterFormContext = createContext<ProductFiltersProContext | null>(null)
|
33
59
|
|
34
|
-
export const
|
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 (!
|
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
|
-
|
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 {
|
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(() => !
|
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
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
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
|
-
|
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,8 @@
|
|
1
1
|
import { ProductListFiltersFragment } from '../ProductListFilters/ProductListFilters.gql'
|
2
|
+
import { ProductFilterEqualChip } from './ProductFilterEqualChip'
|
3
|
+
import { ProductFilterEqualSection } from './ProductFilterEqualSection'
|
4
|
+
import { ProductFilterRangeChip } from './ProductFilterRangeChip'
|
5
|
+
import { ProductFilterRangeSection } from './ProductFilterRangeSection'
|
2
6
|
import { useProductFiltersPro } from './ProductFiltersPro'
|
3
7
|
import { excludeCategory } from './activeAggregations'
|
4
8
|
import { applyAggregationCount } from './applyAggregationCount'
|
@@ -13,32 +17,41 @@ export type ProductFiltersProAggregationsProps = {
|
|
13
17
|
renderer?: FilterRenderer
|
14
18
|
}
|
15
19
|
|
20
|
+
export const productFiltersProSectionRenderer = {
|
21
|
+
FilterRangeTypeInput: ProductFilterRangeSection,
|
22
|
+
FilterEqualTypeInput: ProductFilterEqualSection,
|
23
|
+
}
|
24
|
+
|
25
|
+
export const productFiltersProChipRenderer = {
|
26
|
+
FilterEqualTypeInput: ProductFilterEqualChip,
|
27
|
+
FilterRangeTypeInput: ProductFilterRangeChip,
|
28
|
+
}
|
29
|
+
|
16
30
|
export function ProductFiltersProAggregations(props: ProductFiltersProAggregationsProps) {
|
17
31
|
const { renderer } = props
|
18
32
|
const { params, aggregations, appliedAggregations, filterTypes } = useProductFiltersPro()
|
19
33
|
|
20
34
|
return (
|
21
35
|
<>
|
22
|
-
{excludeCategory(
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
+
{excludeCategory(applyAggregationCount(aggregations, appliedAggregations, params)).map(
|
37
|
+
(aggregation) => {
|
38
|
+
const filterType = filterTypes[aggregation.attribute_code]
|
39
|
+
if (!filterType) return null
|
40
|
+
|
41
|
+
const Component = renderer?.[filterType]
|
42
|
+
if (!Component) {
|
43
|
+
if (process.env.NODE_ENV === 'development') {
|
44
|
+
// eslint-disable-next-line no-console
|
45
|
+
console.log(
|
46
|
+
`The renderer for filterType ${filterType} can not be found, please add it to the renderer prop: renderer={{ ${filterType}: (props) => <>MYRenderer</> }}}}`,
|
47
|
+
)
|
48
|
+
}
|
49
|
+
return null
|
36
50
|
}
|
37
|
-
return null
|
38
|
-
}
|
39
51
|
|
40
|
-
|
41
|
-
|
52
|
+
return <Component key={aggregation.attribute_code} aggregation={aggregation} {...props} />
|
53
|
+
},
|
54
|
+
)}
|
42
55
|
</>
|
43
56
|
)
|
44
57
|
}
|
@@ -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 {
|
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 =
|
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
|
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 &
|
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
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|
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 {
|
7
|
-
import {
|
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
|
18
|
-
const
|
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
|
-
"
|
60
|
-
"
|
61
|
-
"
|
59
|
+
"content" auto
|
60
|
+
"horizontalFilters" auto
|
61
|
+
"beforeContent" auto
|
62
|
+
"items" auto
|
63
|
+
"afterContent" auto
|
62
64
|
`,
|
63
|
-
md: `
|
64
|
-
"topleft
|
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}
|
@@ -11,11 +11,13 @@ import {
|
|
11
11
|
import { Trans } from '@lingui/react'
|
12
12
|
import { useMemo } from 'react'
|
13
13
|
import { useProductFiltersPro } from './ProductFiltersPro'
|
14
|
+
import { SxProps, Theme } from '@mui/material'
|
14
15
|
|
15
|
-
export type ProductFiltersProLimitSectionProps =
|
16
|
+
export type ProductFiltersProLimitSectionProps = { sx?: SxProps<Theme> }
|
16
17
|
|
17
18
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
18
19
|
export function ProductFiltersProLimitSection(props: ProductFiltersProLimitSectionProps) {
|
20
|
+
const { sx } = props
|
19
21
|
const { form } = useProductFiltersPro()
|
20
22
|
const { control } = form
|
21
23
|
const activePageSize = useWatch({ control, name: 'pageSize' })
|
@@ -38,6 +40,7 @@ export function ProductFiltersProLimitSection(props: ProductFiltersProLimitSecti
|
|
38
40
|
|
39
41
|
return (
|
40
42
|
<ActionCardAccordion
|
43
|
+
sx={sx}
|
41
44
|
defaultExpanded={!!activePageSize}
|
42
45
|
summary={<Trans id='Per page' />}
|
43
46
|
details={
|
@@ -48,7 +51,7 @@ export function ProductFiltersProLimitSection(props: ProductFiltersProLimitSecti
|
|
48
51
|
control={control}
|
49
52
|
layout='list'
|
50
53
|
variant='default'
|
51
|
-
size='
|
54
|
+
size='responsive'
|
52
55
|
items={options}
|
53
56
|
/>
|
54
57
|
}
|