@graphcommerce/magento-product 8.1.0-canary.3 → 8.1.0-canary.30
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 -1
- package/Api/ProductPageItem.graphql +1 -1
- package/CHANGELOG.md +155 -2
- package/components/AddProductsToCart/AddProductsToCartButton.tsx +1 -0
- package/components/AddProductsToCart/AddProductsToCartForm.tsx +7 -2
- package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +13 -14
- package/components/AddProductsToCart/UseAddProductsToCartAction.graphql +1 -1
- package/components/AddProductsToCart/findAddedItems.ts +81 -0
- package/components/AddProductsToCart/index.ts +3 -0
- package/components/AddProductsToCart/useAddProductsToCartAction.ts +6 -3
- package/components/AddProductsToCart/useFormAddProductsToCart.ts +1 -2
- package/components/JsonLdProduct/JsonLdProduct.graphql +1 -1
- package/components/JsonLdProduct/JsonLdProductOffer.graphql +2 -3
- package/components/JsonLdProduct/ProductPageJsonLd.tsx +10 -5
- package/components/JsonLdProduct/index.ts +1 -0
- package/components/ProductAddToCart/ProductAddToCart.tsx +6 -4
- package/components/ProductCustomizable/CustomizableAreaOption.tsx +41 -7
- package/components/ProductCustomizable/CustomizableDateOption.tsx +60 -7
- package/components/ProductCustomizable/CustomizableDropDownOption.tsx +63 -15
- package/components/ProductCustomizable/CustomizableFieldOption.tsx +40 -4
- package/components/ProductCustomizable/ProductCustomizable.graphql +1 -1
- package/components/ProductCustomizable/index.ts +1 -0
- package/components/ProductCustomizable/productCustomizableSelectors.ts +59 -0
- package/components/ProductFiltersPro/ProductFiltersPro.tsx +49 -12
- package/components/ProductFiltersPro/ProductFiltersProAggregations.tsx +14 -0
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +10 -10
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +23 -9
- package/components/ProductFiltersPro/ProductFiltersProCategorySection.tsx +70 -0
- package/components/ProductFiltersPro/ProductFiltersProChips.tsx +10 -8
- package/components/ProductFiltersPro/ProductFiltersProLayoutSidebar.tsx +15 -7
- package/components/ProductFiltersPro/ProductFiltersProLimitSection.tsx +4 -1
- package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +9 -28
- package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +15 -0
- package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +12 -32
- package/components/ProductFiltersPro/index.ts +6 -0
- package/components/ProductFiltersPro/useProductFiltersProSort.tsx +76 -0
- package/components/ProductListFiltersContainer/ProductListFiltersContainer.tsx +2 -4
- package/components/ProductListItems/CategoryDefault.graphql +5 -0
- package/components/ProductListItems/ProductListItemsBase.tsx +1 -1
- package/components/ProductListItems/filterTypes.tsx +1 -1
- package/components/ProductListItems/filteredProductList.tsx +1 -1
- package/components/ProductListItems/productListApplyCategoryDefaults.ts +28 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.graphql +3 -0
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumb.tsx +8 -3
- package/components/ProductPageBreadcrumb/ProductPageBreadcrumbs.tsx +40 -0
- package/components/ProductPageBreadcrumb/index.ts +1 -0
- package/components/ProductPageDescription/ComplexTextValue.graphql +1 -1
- package/components/ProductPageGallery/ProductImage.graphql +1 -0
- package/components/ProductPageGallery/ProductPageGallery.tsx +14 -8
- package/components/ProductPagePrice/ProductPagePrice.graphql +3 -0
- package/components/ProductPagePrice/ProductPagePrice.tsx +11 -4
- package/components/ProductPagePrice/useCustomizableOptionPrice.ts +85 -0
- package/components/ProductShortDescription/ProductShortDescription.tsx +2 -0
- package/components/ProductStaticPaths/getProductStaticPaths.ts +2 -3
- package/components/ProductStaticPaths/getSitemapPaths.ts +3 -0
- package/components/index.ts +2 -0
- package/hooks/useProductListLink.ts +10 -5
- package/hooks/useProductListLinkReplace.ts +3 -0
- package/package.json +14 -13
- package/tsconfig.json +1 -1
@@ -4,6 +4,7 @@ import { Trans } from '@lingui/react'
|
|
4
4
|
import { Box } from '@mui/material'
|
5
5
|
import { useFormAddProductsToCart } from '../AddProductsToCart'
|
6
6
|
import { OptionTypeRenderer } from './CustomizableAreaOption'
|
7
|
+
import { Money } from '@graphcommerce/magento-store'
|
7
8
|
|
8
9
|
type CustomizableDateOptionProps = React.ComponentProps<
|
9
10
|
OptionTypeRenderer['CustomizableDateOption']
|
@@ -21,14 +22,56 @@ export function CustomizableDateOption(props: CustomizableDateOptionProps) {
|
|
21
22
|
title,
|
22
23
|
minDate = new Date('1950-11-12T00:00'),
|
23
24
|
maxDate = new Date('9999-11-12T00:00'),
|
25
|
+
dateValue,
|
26
|
+
currency,
|
27
|
+
productPrice,
|
24
28
|
} = props
|
25
|
-
const {
|
26
|
-
|
29
|
+
const {
|
30
|
+
register,
|
31
|
+
setValue,
|
32
|
+
setError,
|
33
|
+
getFieldState,
|
34
|
+
clearErrors,
|
35
|
+
control,
|
36
|
+
resetField,
|
37
|
+
getValues,
|
38
|
+
} = useFormAddProductsToCart()
|
27
39
|
|
28
40
|
const { invalid } = getFieldState(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
29
41
|
|
30
42
|
minDate.setSeconds(0, 0)
|
31
43
|
maxDate.setSeconds(0, 0)
|
44
|
+
if (!dateValue) return null
|
45
|
+
|
46
|
+
const price =
|
47
|
+
dateValue.price === 0
|
48
|
+
? null
|
49
|
+
: dateValue.price && (
|
50
|
+
<Box
|
51
|
+
sx={{
|
52
|
+
display: 'flex',
|
53
|
+
typography: 'body1',
|
54
|
+
'&.sizeMedium': { typographty: 'subtitle1' },
|
55
|
+
'&.sizeLarge': { typography: 'h6' },
|
56
|
+
color:
|
57
|
+
dateValue.uid ===
|
58
|
+
getValues(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
59
|
+
? 'text.primary'
|
60
|
+
: 'text.secondary',
|
61
|
+
}}
|
62
|
+
>
|
63
|
+
{/* Change fontFamily so the + is properly outlined */}
|
64
|
+
<span style={{ fontFamily: 'arial' }}>+{'\u00A0'}</span>
|
65
|
+
<Money
|
66
|
+
value={
|
67
|
+
dateValue.price_type === 'PERCENT'
|
68
|
+
? productPrice * (dateValue.price / 100)
|
69
|
+
: dateValue.price
|
70
|
+
}
|
71
|
+
currency={currency}
|
72
|
+
/>
|
73
|
+
</Box>
|
74
|
+
)
|
32
75
|
return (
|
33
76
|
<Box>
|
34
77
|
<input
|
@@ -41,12 +84,20 @@ export function CustomizableDateOption(props: CustomizableDateOptionProps) {
|
|
41
84
|
<TextFieldElement
|
42
85
|
control={control}
|
43
86
|
name={`cartItems.${index}.entered_options.${optionIndex}.value`}
|
44
|
-
sx={{
|
87
|
+
sx={{
|
88
|
+
width: '100%',
|
89
|
+
'& input[type="datetime-local"]::-webkit-calendar-picker-indicator': {
|
90
|
+
filter: (theme) => (theme.palette.mode === 'dark' ? 'invert(100%)' : 'none'),
|
91
|
+
mr: '10px',
|
92
|
+
},
|
93
|
+
}}
|
94
|
+
defaultValue=''
|
45
95
|
required={!!required}
|
46
96
|
error={invalid}
|
47
97
|
helperText={invalid ? <Trans id='Invalid date' /> : ''}
|
48
98
|
type='datetime-local'
|
49
99
|
InputProps={{
|
100
|
+
endAdornment: price,
|
50
101
|
inputProps: {
|
51
102
|
min: minDate.toISOString().replace(/:00.000Z/, ''),
|
52
103
|
max: maxDate.toISOString().replace(/:00.000Z/, ''),
|
@@ -61,10 +112,12 @@ export function CustomizableDateOption(props: CustomizableDateOptionProps) {
|
|
61
112
|
} else {
|
62
113
|
clearErrors(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
63
114
|
}
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
115
|
+
if (data.currentTarget.value)
|
116
|
+
setValue(
|
117
|
+
`cartItems.${index}.entered_options.${optionIndex}.value`,
|
118
|
+
`${data.currentTarget.value.replace('T', ' ')}:00`,
|
119
|
+
)
|
120
|
+
else resetField(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
68
121
|
}}
|
69
122
|
/>
|
70
123
|
</Box>
|
@@ -1,33 +1,81 @@
|
|
1
|
-
import { SelectElement } from '@graphcommerce/ecommerce-ui'
|
1
|
+
import { SelectElement, useController } from '@graphcommerce/ecommerce-ui'
|
2
2
|
import { SectionHeader, filterNonNullableKeys } from '@graphcommerce/next-ui'
|
3
|
-
import { Box } from '@mui/material'
|
3
|
+
import { Box, ListItemText, MenuItem, TextField, Typography } from '@mui/material'
|
4
4
|
import { useFormAddProductsToCart } from '../AddProductsToCart'
|
5
5
|
import { OptionTypeRenderer } from './CustomizableAreaOption'
|
6
|
+
import { Money } from '@graphcommerce/magento-store'
|
6
7
|
|
7
8
|
type CustomizableDropDownOptionProps = React.ComponentProps<
|
8
9
|
OptionTypeRenderer['CustomizableDropDownOption']
|
9
10
|
>
|
10
11
|
|
11
12
|
export function CustomizableDropDownOption(props: CustomizableDropDownOptionProps) {
|
12
|
-
const { uid, required, index, title, dropdownValue } = props
|
13
|
-
const { control } = useFormAddProductsToCart()
|
13
|
+
const { uid, required, index, title, dropdownValue, productPrice, currency } = props
|
14
|
+
const { control, getValues } = useFormAddProductsToCart()
|
15
|
+
|
16
|
+
const {
|
17
|
+
field: { onChange, value, ref, ...field },
|
18
|
+
fieldState: { invalid, error },
|
19
|
+
} = useController({
|
20
|
+
name: `cartItems.${index}.customizable_options.${uid}`,
|
21
|
+
rules: {
|
22
|
+
required: Boolean(required),
|
23
|
+
},
|
24
|
+
control,
|
25
|
+
defaultValue: '',
|
26
|
+
})
|
14
27
|
|
15
28
|
return (
|
16
29
|
<Box>
|
17
30
|
<SectionHeader labelLeft={title} sx={{ mt: 0 }} />
|
18
|
-
|
19
|
-
|
31
|
+
|
32
|
+
<TextField
|
33
|
+
sx={{
|
34
|
+
width: '100%',
|
35
|
+
'& .MuiSelect-select': {
|
36
|
+
display: 'flex',
|
37
|
+
justifyContent: 'space-between',
|
38
|
+
alignItems: 'center',
|
39
|
+
},
|
40
|
+
}}
|
20
41
|
color='primary'
|
21
|
-
|
22
|
-
|
23
|
-
|
42
|
+
value={value ?? ''}
|
43
|
+
{...field}
|
44
|
+
inputRef={ref}
|
45
|
+
onChange={(event) => onChange(event.target.value)}
|
46
|
+
select
|
24
47
|
required={Boolean(required)}
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
48
|
+
error={invalid}
|
49
|
+
helperText={error?.message}
|
50
|
+
>
|
51
|
+
{filterNonNullableKeys(dropdownValue, ['title']).map((option) => (
|
52
|
+
<MenuItem key={option.uid} value={option.uid}>
|
53
|
+
<Box>{option.title}</Box>
|
54
|
+
|
55
|
+
{option.price ? (
|
56
|
+
<Box
|
57
|
+
sx={{
|
58
|
+
// display: 'flex',
|
59
|
+
typography: 'body1',
|
60
|
+
'&.sizeMedium': { typographty: 'subtitle1' },
|
61
|
+
'&.sizeLarge': { typography: 'h6' },
|
62
|
+
color: option.uid === value ? 'text.primary' : 'text.secondary',
|
63
|
+
}}
|
64
|
+
>
|
65
|
+
<span style={{ fontFamily: 'arial', paddingTop: '1px' }}>+ </span>
|
66
|
+
<Money
|
67
|
+
value={
|
68
|
+
option.price_type === 'PERCENT'
|
69
|
+
? productPrice * (option.price / 100)
|
70
|
+
: option.price
|
71
|
+
}
|
72
|
+
currency={currency}
|
73
|
+
/>
|
74
|
+
</Box>
|
75
|
+
) : null}
|
76
|
+
</MenuItem>
|
77
|
+
))}
|
78
|
+
</TextField>
|
31
79
|
</Box>
|
32
80
|
)
|
33
81
|
}
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Money } from '@graphcommerce/magento-store'
|
1
2
|
import { TextFieldElement } from '@graphcommerce/ecommerce-ui'
|
2
3
|
import { SectionHeader } from '@graphcommerce/next-ui'
|
3
4
|
import { i18n } from '@lingui/core'
|
@@ -10,10 +11,12 @@ type CustomizableFieldOptionProps = React.ComponentProps<
|
|
10
11
|
>
|
11
12
|
|
12
13
|
export function CustomizableFieldOption(props: CustomizableFieldOptionProps) {
|
13
|
-
const { uid, required, optionIndex, index, title, fieldValue } = props
|
14
|
-
const { control, register } = useFormAddProductsToCart()
|
14
|
+
const { uid, required, optionIndex, index, title, fieldValue, productPrice, currency } = props
|
15
|
+
const { control, register, resetField, getValues } = useFormAddProductsToCart()
|
15
16
|
|
16
|
-
|
17
|
+
if (!fieldValue) return null
|
18
|
+
|
19
|
+
const maxLength = fieldValue.max_characters ?? 0
|
17
20
|
return (
|
18
21
|
<Box>
|
19
22
|
<SectionHeader labelLeft={title} sx={{ mt: 0 }} />
|
@@ -29,7 +32,36 @@ export function CustomizableFieldOption(props: CustomizableFieldOptionProps) {
|
|
29
32
|
control={control}
|
30
33
|
name={`cartItems.${index}.entered_options.${optionIndex}.value`}
|
31
34
|
required={Boolean(required)}
|
32
|
-
|
35
|
+
InputProps={{
|
36
|
+
endAdornment:
|
37
|
+
fieldValue.price === 0
|
38
|
+
? null
|
39
|
+
: fieldValue.price && (
|
40
|
+
<Box
|
41
|
+
sx={{
|
42
|
+
display: 'flex',
|
43
|
+
typography: 'body1',
|
44
|
+
'&.sizeMedium': { typographty: 'subtitle1' },
|
45
|
+
'&.sizeLarge': { typography: 'h6' },
|
46
|
+
color: getValues(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
47
|
+
? 'text.primary'
|
48
|
+
: 'text.secondary',
|
49
|
+
}}
|
50
|
+
>
|
51
|
+
{/* Change fontFamily so the + is properly outlined */}
|
52
|
+
<span style={{ fontFamily: 'arial', paddingTop: '1px' }}>+{'\u00A0'}</span>
|
53
|
+
<Money
|
54
|
+
value={
|
55
|
+
fieldValue.price_type === 'PERCENT'
|
56
|
+
? productPrice * (fieldValue.price / 100)
|
57
|
+
: fieldValue.price
|
58
|
+
}
|
59
|
+
currency={currency}
|
60
|
+
/>
|
61
|
+
</Box>
|
62
|
+
),
|
63
|
+
}}
|
64
|
+
rules={{
|
33
65
|
maxLength: {
|
34
66
|
value: maxLength,
|
35
67
|
message: i18n._(/* i18n*/ 'There is a maximum of ‘{maxLength}’ characters', {
|
@@ -43,6 +75,10 @@ export function CustomizableFieldOption(props: CustomizableFieldOptionProps) {
|
|
43
75
|
maxLength,
|
44
76
|
})
|
45
77
|
}
|
78
|
+
onChange={(data) => {
|
79
|
+
if (!data.currentTarget.value)
|
80
|
+
resetField(`cartItems.${index}.entered_options.${optionIndex}.value`)
|
81
|
+
}}
|
46
82
|
/>
|
47
83
|
</Box>
|
48
84
|
)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import type { PriceTypeEnum } from '@graphcommerce/graphql-mesh'
|
2
|
+
import type { Simplify } from 'type-fest'
|
3
|
+
import type { CustomizableAreaOptionFragment } from './CustomizableAreaOption.gql'
|
4
|
+
import type { CustomizableCheckboxOptionFragment } from './CustomizableCheckboxOption.gql'
|
5
|
+
import type { CustomizableDateOptionFragment } from './CustomizableDateOption.gql'
|
6
|
+
import type { CustomizableDropDownOptionFragment } from './CustomizableDropDownOption.gql'
|
7
|
+
import type { CustomizableFieldOptionFragment } from './CustomizableFieldOption.gql'
|
8
|
+
import type { CustomizableFileOptionFragment } from './CustomizableFileOption.gql'
|
9
|
+
import type { CustomizableMultipleOptionFragment } from './CustomizableMultipleOption.gql'
|
10
|
+
import type { CustomizableRadioOptionFragment } from './CustomizableRadioOption.gql'
|
11
|
+
import type { ProductCustomizable_SimpleProduct_Fragment } from './ProductCustomizable.gql'
|
12
|
+
|
13
|
+
export type CustomizableProductOptionBase =
|
14
|
+
| {
|
15
|
+
price?: number | null | undefined
|
16
|
+
price_type?: PriceTypeEnum | null | undefined
|
17
|
+
uid?: string | null | undefined
|
18
|
+
}
|
19
|
+
| undefined
|
20
|
+
| null
|
21
|
+
|
22
|
+
export type AnyOption = NonNullable<
|
23
|
+
NonNullable<ProductCustomizable_SimpleProduct_Fragment['options']>[number]
|
24
|
+
>
|
25
|
+
|
26
|
+
export type OptionValueSelector = {
|
27
|
+
[T in AnyOption as T['__typename']]: (
|
28
|
+
option: T,
|
29
|
+
) => CustomizableProductOptionBase | CustomizableProductOptionBase[]
|
30
|
+
}
|
31
|
+
|
32
|
+
type MissingOptionValueSelectors = Omit<
|
33
|
+
OptionValueSelector,
|
34
|
+
keyof typeof productCustomizableSelectors
|
35
|
+
>
|
36
|
+
type DefinedOptionValueSelectors = Partial<
|
37
|
+
Pick<OptionValueSelector, keyof typeof productCustomizableSelectors>
|
38
|
+
>
|
39
|
+
|
40
|
+
type Selectors = Simplify<
|
41
|
+
keyof MissingOptionValueSelectors extends never
|
42
|
+
? (MissingOptionValueSelectors & DefinedOptionValueSelectors) | undefined
|
43
|
+
: MissingOptionValueSelectors & DefinedOptionValueSelectors
|
44
|
+
>
|
45
|
+
|
46
|
+
export const productCustomizableSelectors = {
|
47
|
+
CustomizableAreaOption: (o: CustomizableAreaOptionFragment) => o.areaValue,
|
48
|
+
CustomizableCheckboxOption: (o: CustomizableCheckboxOptionFragment) => o.checkboxValue,
|
49
|
+
CustomizableFileOption: (o: CustomizableFileOptionFragment) => o.fileValue,
|
50
|
+
CustomizableDateOption: (o: CustomizableDateOptionFragment) => o.dateValue,
|
51
|
+
CustomizableDropDownOption: (o: CustomizableDropDownOptionFragment) => o.dropdownValue,
|
52
|
+
CustomizableFieldOption: (o: CustomizableFieldOptionFragment) => o.fieldValue,
|
53
|
+
CustomizableMultipleOption: (o: CustomizableMultipleOptionFragment) => o.multipleValue,
|
54
|
+
CustomizableRadioOption: (o: CustomizableRadioOptionFragment) => o.radioValue,
|
55
|
+
}
|
56
|
+
|
57
|
+
export type SelectorsProp = keyof MissingOptionValueSelectors extends never
|
58
|
+
? { selectors?: Selectors }
|
59
|
+
: { selectors: Selectors }
|
@@ -1,14 +1,15 @@
|
|
1
|
-
import { useForm, UseFormProps, UseFormReturn } from '@graphcommerce/ecommerce-ui'
|
2
|
-
import { useMemoObject } from '@graphcommerce/next-ui'
|
3
|
-
import { useEventCallback } from '@mui/material'
|
4
|
-
import
|
5
|
-
import {
|
1
|
+
import { FormAutoSubmit, useForm, UseFormProps, UseFormReturn } from '@graphcommerce/ecommerce-ui'
|
2
|
+
import { useMatchMediaMotionValue, useMemoObject } from '@graphcommerce/next-ui'
|
3
|
+
import { Theme, useEventCallback, useMediaQuery, useTheme } from '@mui/material'
|
4
|
+
import { m, useTransform } from 'framer-motion'
|
5
|
+
import { useRouter } from 'next/router'
|
6
|
+
import React, { BaseSyntheticEvent, createContext, useContext, useMemo, useRef } from 'react'
|
7
|
+
import { productListLinkFromFilter } from '../../hooks/useProductListLink'
|
6
8
|
import { ProductListFiltersFragment } from '../ProductListFilters/ProductListFilters.gql'
|
7
9
|
import {
|
8
10
|
ProductFilterParams,
|
9
11
|
ProductListParams,
|
10
12
|
toFilterParams,
|
11
|
-
toProductListParams,
|
12
13
|
} from '../ProductListItems/filterTypes'
|
13
14
|
|
14
15
|
type DataProps = {
|
@@ -42,19 +43,54 @@ export type FilterFormProviderProps = Omit<
|
|
42
43
|
> & {
|
43
44
|
children: React.ReactNode
|
44
45
|
params: ProductListParams
|
46
|
+
/**
|
47
|
+
* Whether the filter should scroll to the products list and whether to submit the form on change.
|
48
|
+
*/
|
49
|
+
autoSubmitMd?: boolean
|
45
50
|
} & DataProps
|
46
51
|
|
52
|
+
function AutoSubmitSidebarDesktop() {
|
53
|
+
const { form, submit } = useProductFiltersPro()
|
54
|
+
|
55
|
+
// We only need to auto-submit when the layout is not sidebar and we're viewing on desktop
|
56
|
+
const autoSubmitDisabled = useMediaQuery<Theme>((t) => t.breakpoints.down('md'), {
|
57
|
+
defaultMatches: false,
|
58
|
+
})
|
59
|
+
|
60
|
+
return <FormAutoSubmit control={form.control} disabled={autoSubmitDisabled} submit={submit} />
|
61
|
+
}
|
62
|
+
|
47
63
|
export function ProductFiltersPro(props: FilterFormProviderProps) {
|
48
|
-
const {
|
64
|
+
const {
|
65
|
+
children,
|
66
|
+
params,
|
67
|
+
aggregations,
|
68
|
+
appliedAggregations,
|
69
|
+
filterTypes,
|
70
|
+
autoSubmitMd = false,
|
71
|
+
...formProps
|
72
|
+
} = props
|
49
73
|
|
50
74
|
const defaultValues = useMemoObject(toFilterParams(params))
|
51
75
|
const form = useForm<ProductFilterParams>({ defaultValues, ...formProps })
|
76
|
+
const ref = useRef<HTMLFormElement>(null)
|
77
|
+
|
78
|
+
const router = useRouter()
|
79
|
+
const theme = useTheme()
|
80
|
+
const isDesktop = useMatchMediaMotionValue('up', 'md')
|
81
|
+
const scrollMarginTop = useTransform(() => (isDesktop.get() ? 0 : theme.appShell.headerHeightSm))
|
82
|
+
const scroll = useTransform(() => !autoSubmitMd || isDesktop.get())
|
52
83
|
|
53
|
-
const push = useProductListLinkReplace({ scroll: false })
|
54
84
|
const submit = useEventCallback(
|
55
|
-
form.handleSubmit(async (formValues) =>
|
56
|
-
|
57
|
-
|
85
|
+
form.handleSubmit(async (formValues) => {
|
86
|
+
const path = productListLinkFromFilter({ ...formValues, currentPage: 1 })
|
87
|
+
if (router.asPath === path) return false
|
88
|
+
|
89
|
+
const opts = { scroll: scroll.get() }
|
90
|
+
return (router.query.url ?? []).includes('q')
|
91
|
+
? router.replace(path, path, opts)
|
92
|
+
: router.push(path, path, opts)
|
93
|
+
}),
|
58
94
|
)
|
59
95
|
|
60
96
|
const filterFormContext: FilterFormContextProps = useMemo(
|
@@ -71,7 +107,8 @@ export function ProductFiltersPro(props: FilterFormProviderProps) {
|
|
71
107
|
|
72
108
|
return (
|
73
109
|
<FilterFormContext.Provider value={filterFormContext}>
|
74
|
-
<form noValidate onSubmit={submit} id='products' />
|
110
|
+
<m.form ref={ref} noValidate onSubmit={submit} id='products' style={{ scrollMarginTop }} />
|
111
|
+
{autoSubmitMd && <AutoSubmitSidebarDesktop />}
|
75
112
|
{children}
|
76
113
|
</FilterFormContext.Provider>
|
77
114
|
)
|
@@ -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,6 +17,16 @@ 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()
|
@@ -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 {
|
@@ -23,13 +22,8 @@ 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
|
-
const { sort_fields, total_count, renderer, ...rest } = props
|
26
|
+
const { sort_fields, total_count, renderer, category, ...rest } = props
|
33
27
|
|
34
28
|
const { submit, params, aggregations, appliedAggregations } = useProductFiltersPro()
|
35
29
|
const { sort } = params
|
@@ -59,9 +53,15 @@ export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFilte
|
|
59
53
|
>
|
60
54
|
{() => (
|
61
55
|
<>
|
62
|
-
<ProductFiltersProSortSection
|
56
|
+
<ProductFiltersProSortSection
|
57
|
+
sort_fields={sort_fields}
|
58
|
+
total_count={total_count}
|
59
|
+
category={category}
|
60
|
+
/>
|
63
61
|
<ProductFiltersProLimitSection />
|
64
|
-
<ProductFiltersProAggregations
|
62
|
+
<ProductFiltersProAggregations
|
63
|
+
renderer={{ ...productFiltersProSectionRenderer, ...renderer }}
|
64
|
+
/>
|
65
65
|
</>
|
66
66
|
)}
|
67
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,21 +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 = [] } = 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])]}>
|
27
|
-
<
|
34
|
+
<ProductFiltersProCategorySection category={category} params={params} />
|
35
|
+
<ProductFiltersProSortSection
|
36
|
+
sort_fields={sort_fields}
|
37
|
+
total_count={total_count}
|
38
|
+
category={category}
|
39
|
+
/>
|
28
40
|
<ProductFiltersProLimitSection />
|
29
|
-
<ProductFiltersProAggregations
|
41
|
+
<ProductFiltersProAggregations
|
42
|
+
renderer={{ ...productFiltersProSectionRenderer, ...renderer }}
|
43
|
+
/>
|
30
44
|
</Box>
|
31
45
|
)
|
32
46
|
}
|
@@ -0,0 +1,70 @@
|
|
1
|
+
import { UseCategoryTreeProps, useCategoryTree } from '@graphcommerce/magento-category'
|
2
|
+
import {
|
3
|
+
ActionCard,
|
4
|
+
ActionCardAccordion,
|
5
|
+
ActionCardList,
|
6
|
+
IconSvg,
|
7
|
+
iconChevronLeft,
|
8
|
+
responsiveVal,
|
9
|
+
} from '@graphcommerce/next-ui'
|
10
|
+
import { Trans } from '@lingui/react'
|
11
|
+
import { Box, SxProps, Theme } from '@mui/material'
|
12
|
+
import { useRouter } from 'next/router'
|
13
|
+
|
14
|
+
export type ProductFiltersCategorySectionProps = UseCategoryTreeProps & {
|
15
|
+
hideTitle?: boolean
|
16
|
+
sx?: SxProps<Theme>
|
17
|
+
}
|
18
|
+
|
19
|
+
export function ProductFiltersProCategorySection(props: ProductFiltersCategorySectionProps) {
|
20
|
+
const { hideTitle, sx } = props
|
21
|
+
const router = useRouter()
|
22
|
+
const categoryTree = useCategoryTree(props)
|
23
|
+
|
24
|
+
if (!categoryTree) return null
|
25
|
+
|
26
|
+
return (
|
27
|
+
<ActionCardAccordion
|
28
|
+
sx={[
|
29
|
+
hideTitle ? { '& .MuiAccordionSummary-root': { display: 'none' } } : {},
|
30
|
+
sx,
|
31
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
32
|
+
]}
|
33
|
+
defaultExpanded
|
34
|
+
summary={<Trans id='Categories' />}
|
35
|
+
details={
|
36
|
+
<ActionCardList value='cat' variant='default'>
|
37
|
+
{categoryTree.map((item) => (
|
38
|
+
<ActionCard
|
39
|
+
{...item}
|
40
|
+
title={
|
41
|
+
item.isBack ? (
|
42
|
+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
43
|
+
<IconSvg src={iconChevronLeft} size='medium' />
|
44
|
+
{item.title}
|
45
|
+
</Box>
|
46
|
+
) : (
|
47
|
+
item.title
|
48
|
+
)
|
49
|
+
}
|
50
|
+
sx={[
|
51
|
+
item.isBack ? {} : {},
|
52
|
+
{
|
53
|
+
'&.sizeSmall': { pl: responsiveVal(8 * item.indent, 12 * item.indent) },
|
54
|
+
'&.sizeMedium': { pl: responsiveVal(10 * item.indent, 14 * item.indent) },
|
55
|
+
'&.sizeLarge': { pl: responsiveVal(12 * item.indent, 16 * item.indent) },
|
56
|
+
'&.sizeResponsive': { pl: responsiveVal(8 * item.indent, 16 * item.indent) },
|
57
|
+
},
|
58
|
+
]}
|
59
|
+
value={item.href}
|
60
|
+
key={item.href}
|
61
|
+
selected={item.selected}
|
62
|
+
onClick={() => router.push(item.href)}
|
63
|
+
/>
|
64
|
+
))}
|
65
|
+
</ActionCardList>
|
66
|
+
}
|
67
|
+
right={undefined}
|
68
|
+
/>
|
69
|
+
)
|
70
|
+
}
|
@@ -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
|
}
|