@graphcommerce/magento-product 8.1.0-canary.3 → 8.1.0-canary.5
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/CHANGELOG.md +95 -2
- package/components/AddProductsToCart/AddProductsToCartSnackbar.tsx +13 -14
- package/components/AddProductsToCart/findAddedItems.ts +81 -0
- package/components/AddProductsToCart/index.ts +3 -0
- package/components/AddProductsToCart/useAddProductsToCartAction.ts +4 -2
- package/components/JsonLdProduct/JsonLdProductOffer.graphql +2 -3
- package/components/JsonLdProduct/ProductPageJsonLd.tsx +9 -4
- 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/ProductFiltersPro/ProductFiltersPro.tsx +25 -10
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersChip.tsx +6 -2
- package/components/ProductFiltersPro/ProductFiltersProAllFiltersSidebar.tsx +6 -2
- package/components/ProductFiltersPro/ProductFiltersProSortChip.tsx +9 -28
- package/components/ProductFiltersPro/ProductFiltersProSortDirectionArrow.tsx +17 -0
- package/components/ProductFiltersPro/ProductFiltersProSortSection.tsx +7 -32
- package/components/ProductFiltersPro/useProductFiltersProSort.tsx +74 -0
- 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.tsx +5 -3
- package/components/ProductPagePrice/ProductPagePrice.graphql +3 -0
- package/components/ProductPagePrice/ProductPagePrice.tsx +11 -4
- package/components/ProductPagePrice/useCustomizableOptionPrice.ts +127 -0
- package/components/index.ts +2 -0
- package/hooks/useProductListLink.ts +10 -5
- package/hooks/useProductListLinkReplace.ts +3 -0
- package/package.json +13 -13
- package/tsconfig.json +1 -1
@@ -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
|
)
|
@@ -1,14 +1,15 @@
|
|
1
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 {
|
2
|
+
import { useMatchMediaMotionValue, useMemoObject } from '@graphcommerce/next-ui'
|
3
|
+
import { useEventCallback, 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 = {
|
@@ -44,17 +45,31 @@ export type FilterFormProviderProps = Omit<
|
|
44
45
|
params: ProductListParams
|
45
46
|
} & DataProps
|
46
47
|
|
48
|
+
const isSidebar = import.meta.graphCommerce.productFiltersLayout === 'SIDEBAR'
|
49
|
+
|
47
50
|
export function ProductFiltersPro(props: FilterFormProviderProps) {
|
48
51
|
const { children, params, aggregations, appliedAggregations, filterTypes, ...formProps } = props
|
49
52
|
|
50
53
|
const defaultValues = useMemoObject(toFilterParams(params))
|
51
54
|
const form = useForm<ProductFilterParams>({ defaultValues, ...formProps })
|
55
|
+
const ref = useRef<HTMLFormElement>(null)
|
56
|
+
|
57
|
+
const router = useRouter()
|
58
|
+
const theme = useTheme()
|
59
|
+
const isDesktop = useMatchMediaMotionValue('up', 'md')
|
60
|
+
const scrollMarginTop = useTransform(() => (isDesktop.get() ? 0 : theme.appShell.headerHeightSm))
|
61
|
+
const scroll = useTransform(() => !isSidebar || isDesktop.get())
|
52
62
|
|
53
|
-
const push = useProductListLinkReplace({ scroll: false })
|
54
63
|
const submit = useEventCallback(
|
55
|
-
form.handleSubmit(async (formValues) =>
|
56
|
-
|
57
|
-
|
64
|
+
form.handleSubmit(async (formValues) => {
|
65
|
+
const path = productListLinkFromFilter({ ...formValues, currentPage: 1 })
|
66
|
+
if (router.asPath === path) return false
|
67
|
+
|
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)
|
72
|
+
}),
|
58
73
|
)
|
59
74
|
|
60
75
|
const filterFormContext: FilterFormContextProps = useMemo(
|
@@ -71,7 +86,7 @@ export function ProductFiltersPro(props: FilterFormProviderProps) {
|
|
71
86
|
|
72
87
|
return (
|
73
88
|
<FilterFormContext.Provider value={filterFormContext}>
|
74
|
-
<form noValidate onSubmit={submit} id='products' />
|
89
|
+
<m.form ref={ref} noValidate onSubmit={submit} id='products' style={{ scrollMarginTop }} />
|
75
90
|
{children}
|
76
91
|
</FilterFormContext.Provider>
|
77
92
|
)
|
@@ -29,7 +29,7 @@ const defaultRenderer = {
|
|
29
29
|
}
|
30
30
|
|
31
31
|
export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFiltersChipProps) {
|
32
|
-
const { sort_fields, total_count, renderer, ...rest } = props
|
32
|
+
const { sort_fields, total_count, renderer, category, ...rest } = props
|
33
33
|
|
34
34
|
const { submit, params, aggregations, appliedAggregations } = useProductFiltersPro()
|
35
35
|
const { sort } = params
|
@@ -59,7 +59,11 @@ export function ProductFiltersProAllFiltersChip(props: ProductFiltersProAllFilte
|
|
59
59
|
>
|
60
60
|
{() => (
|
61
61
|
<>
|
62
|
-
<ProductFiltersProSortSection
|
62
|
+
<ProductFiltersProSortSection
|
63
|
+
sort_fields={sort_fields}
|
64
|
+
total_count={total_count}
|
65
|
+
category={category}
|
66
|
+
/>
|
63
67
|
<ProductFiltersProLimitSection />
|
64
68
|
<ProductFiltersProAggregations renderer={{ ...defaultRenderer, ...renderer }} />
|
65
69
|
</>
|
@@ -20,11 +20,15 @@ const defaultRenderer = {
|
|
20
20
|
}
|
21
21
|
|
22
22
|
export function ProductFiltersProAllFiltersSidebar(props: ProductFiltersProAllFiltersSidebarProps) {
|
23
|
-
const { sort_fields, total_count, renderer, sx = [] } = props
|
23
|
+
const { sort_fields, total_count, renderer, sx = [], category } = props
|
24
24
|
|
25
25
|
return (
|
26
26
|
<Box sx={[{ display: { xs: 'none', md: 'grid' } }, ...(Array.isArray(sx) ? sx : [sx])]}>
|
27
|
-
<ProductFiltersProSortSection
|
27
|
+
<ProductFiltersProSortSection
|
28
|
+
sort_fields={sort_fields}
|
29
|
+
total_count={total_count}
|
30
|
+
category={category}
|
31
|
+
/>
|
28
32
|
<ProductFiltersProLimitSection />
|
29
33
|
<ProductFiltersProAggregations renderer={{ ...defaultRenderer, ...renderer }} />
|
30
34
|
</Box>
|
@@ -1,53 +1,34 @@
|
|
1
|
-
import { useWatch } from '@graphcommerce/ecommerce-ui'
|
2
|
-
import { useQuery } from '@graphcommerce/graphql'
|
3
|
-
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
4
1
|
import {
|
5
2
|
ActionCard,
|
6
3
|
ActionCardListForm,
|
7
4
|
ChipOverlayOrPopper,
|
8
5
|
ChipOverlayOrPopperProps,
|
9
|
-
filterNonNullableKeys,
|
10
6
|
} from '@graphcommerce/next-ui'
|
11
7
|
import { Trans } from '@lingui/react'
|
12
|
-
import { useMemo } from 'react'
|
13
|
-
import { ProductListSortFragment } from '../ProductListSort/ProductListSort.gql'
|
14
8
|
import { useProductFiltersPro } from './ProductFiltersPro'
|
9
|
+
import { UseProductFiltersProSortProps, useProductFiltersProSort } from './useProductFiltersProSort'
|
15
10
|
|
16
|
-
export type ProductListActionSortProps =
|
11
|
+
export type ProductListActionSortProps = UseProductFiltersProSortProps &
|
17
12
|
Omit<
|
18
13
|
ChipOverlayOrPopperProps,
|
19
14
|
'label' | 'selected' | 'selectedLabel' | 'onApply' | 'onReset' | 'onClose' | 'children'
|
20
15
|
>
|
21
16
|
|
22
17
|
export function ProductFiltersProSortChip(props: ProductListActionSortProps) {
|
23
|
-
const { sort_fields, chipProps, ...rest } = props
|
24
|
-
const {
|
25
|
-
const {
|
26
|
-
const activeSort = useWatch({ control, name: 'sort' })
|
27
|
-
|
28
|
-
const { data: storeConfigQuery } = useQuery(StoreConfigDocument)
|
29
|
-
const defaultSort = storeConfigQuery?.storeConfig?.catalog_default_sort_by
|
30
|
-
|
31
|
-
const options = useMemo(
|
32
|
-
() =>
|
33
|
-
filterNonNullableKeys(sort_fields?.options, ['value', 'label']).map((option) => ({
|
34
|
-
...option,
|
35
|
-
value: option.value === defaultSort ? null : option.value,
|
36
|
-
title: option.label,
|
37
|
-
})),
|
38
|
-
[defaultSort, sort_fields?.options],
|
39
|
-
)
|
18
|
+
const { sort_fields, chipProps, category, ...rest } = props
|
19
|
+
const { submit, form } = useProductFiltersPro()
|
20
|
+
const { options, showReset, selected, selectedLabel } = useProductFiltersProSort(props)
|
40
21
|
|
41
22
|
return (
|
42
23
|
<ChipOverlayOrPopper
|
43
24
|
{...rest}
|
44
25
|
overlayProps={{ sizeSm: 'minimal', sizeMd: 'minimal', ...rest.overlayProps }}
|
45
26
|
label={<Trans id='Sort By' />}
|
46
|
-
selected={
|
47
|
-
selectedLabel={
|
27
|
+
selected={selected}
|
28
|
+
selectedLabel={selectedLabel}
|
48
29
|
onApply={submit}
|
49
30
|
onReset={
|
50
|
-
|
31
|
+
showReset
|
51
32
|
? () => {
|
52
33
|
form.setValue('sort', null)
|
53
34
|
form.setValue('dir', null)
|
@@ -60,7 +41,7 @@ export function ProductFiltersProSortChip(props: ProductListActionSortProps) {
|
|
60
41
|
>
|
61
42
|
{() => (
|
62
43
|
<ActionCardListForm
|
63
|
-
control={control}
|
44
|
+
control={form.control}
|
64
45
|
name='sort'
|
65
46
|
layout='list'
|
66
47
|
variant='default'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { SortEnum } from '@graphcommerce/graphql-mesh'
|
2
|
+
import { IconSvg } from '@graphcommerce/next-ui'
|
3
|
+
import * as IconArrowDown from '@graphcommerce/next-ui/icons/arrow-down.svg'
|
4
|
+
import * as IconArrowUp from '@graphcommerce/next-ui/icons/arrow-up.svg'
|
5
|
+
|
6
|
+
type Props = {
|
7
|
+
sortDirection: SortEnum | null
|
8
|
+
}
|
9
|
+
|
10
|
+
export function ProductFiltersProSortDirectionArrow({ sortDirection }: Props) {
|
11
|
+
return (
|
12
|
+
<IconSvg
|
13
|
+
src={sortDirection === 'ASC' || sortDirection === null ? IconArrowUp : IconArrowDown}
|
14
|
+
sx={{ display: 'flex' }}
|
15
|
+
/>
|
16
|
+
)
|
17
|
+
}
|
@@ -1,46 +1,21 @@
|
|
1
|
-
import {
|
2
|
-
import { useQuery } from '@graphcommerce/graphql'
|
3
|
-
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
4
|
-
import {
|
5
|
-
ActionCard,
|
6
|
-
ActionCardAccordion,
|
7
|
-
ActionCardListForm,
|
8
|
-
Button,
|
9
|
-
filterNonNullableKeys,
|
10
|
-
} from '@graphcommerce/next-ui'
|
1
|
+
import { ActionCard, ActionCardAccordion, ActionCardListForm, Button } from '@graphcommerce/next-ui'
|
11
2
|
import { Trans } from '@lingui/react'
|
12
|
-
import { useMemo } from 'react'
|
13
|
-
import { ProductListSortFragment } from '../ProductListSort/ProductListSort.gql'
|
14
3
|
import { useProductFiltersPro } from './ProductFiltersPro'
|
4
|
+
import { UseProductFiltersProSortProps, useProductFiltersProSort } from './useProductFiltersProSort'
|
15
5
|
|
16
|
-
export type ProductFiltersProSortSectionProps =
|
6
|
+
export type ProductFiltersProSortSectionProps = UseProductFiltersProSortProps
|
17
7
|
|
18
8
|
export function ProductFiltersProSortSection(props: ProductFiltersProSortSectionProps) {
|
19
|
-
const { sort_fields } = props
|
20
9
|
const { form } = useProductFiltersPro()
|
21
|
-
const {
|
22
|
-
const activeSort = useWatch({ control, name: 'sort' })
|
23
|
-
|
24
|
-
const { data: storeConfigQuery } = useQuery(StoreConfigDocument)
|
25
|
-
const defaultSort = storeConfigQuery?.storeConfig?.catalog_default_sort_by
|
26
|
-
|
27
|
-
const options = useMemo(
|
28
|
-
() =>
|
29
|
-
filterNonNullableKeys(sort_fields?.options, ['value', 'label']).map((option) => ({
|
30
|
-
...option,
|
31
|
-
value: option.value === defaultSort ? null : option.value,
|
32
|
-
title: option.label,
|
33
|
-
})),
|
34
|
-
[defaultSort, sort_fields?.options],
|
35
|
-
)
|
10
|
+
const { options, showReset, selected } = useProductFiltersProSort(props)
|
36
11
|
|
37
12
|
return (
|
38
13
|
<ActionCardAccordion
|
39
|
-
defaultExpanded={
|
14
|
+
defaultExpanded={selected}
|
40
15
|
summary={<Trans id='Sort By' />}
|
41
16
|
details={
|
42
17
|
<ActionCardListForm
|
43
|
-
control={control}
|
18
|
+
control={form.control}
|
44
19
|
name='sort'
|
45
20
|
layout='list'
|
46
21
|
variant='default'
|
@@ -50,7 +25,7 @@ export function ProductFiltersProSortSection(props: ProductFiltersProSortSection
|
|
50
25
|
/>
|
51
26
|
}
|
52
27
|
right={
|
53
|
-
|
28
|
+
showReset ? (
|
54
29
|
<Button
|
55
30
|
color='primary'
|
56
31
|
onClick={(e) => {
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import { useWatch } from '@graphcommerce/ecommerce-ui'
|
2
|
+
import { useQuery } from '@graphcommerce/graphql'
|
3
|
+
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
4
|
+
import { filterNonNullableKeys } from '@graphcommerce/next-ui'
|
5
|
+
import { i18n } from '@lingui/core'
|
6
|
+
import { useMemo } from 'react'
|
7
|
+
import { CategoryDefaultFragment } from '../ProductListItems/CategoryDefault.gql'
|
8
|
+
import { ProductFilterParams } from '../ProductListItems/filterTypes'
|
9
|
+
import { ProductListSortFragment } from '../ProductListSort'
|
10
|
+
import { useProductFiltersPro } from './ProductFiltersPro'
|
11
|
+
import type { ProductListActionSortProps } from './ProductFiltersProSortChip'
|
12
|
+
import { ProductFiltersProSortDirectionArrow } from './ProductFiltersProSortDirectionArrow'
|
13
|
+
|
14
|
+
const exclude = ['relevance', 'position']
|
15
|
+
|
16
|
+
export type UseProductFiltersProSortProps = ProductListSortFragment & {
|
17
|
+
category?: CategoryDefaultFragment
|
18
|
+
}
|
19
|
+
|
20
|
+
export function useProductFiltersProSort(props: ProductListActionSortProps) {
|
21
|
+
const { sort_fields, category } = props
|
22
|
+
|
23
|
+
const { params, form } = useProductFiltersPro()
|
24
|
+
const { control, setValue } = form
|
25
|
+
|
26
|
+
const sortFields = useMemo(
|
27
|
+
() =>
|
28
|
+
filterNonNullableKeys(sort_fields?.options).map((o) =>
|
29
|
+
!category?.uid && o.value === 'position'
|
30
|
+
? { value: 'relevance', label: i18n._('Relevance') }
|
31
|
+
: o,
|
32
|
+
),
|
33
|
+
[category?.uid, sort_fields?.options],
|
34
|
+
)
|
35
|
+
const availableSortBy = category?.available_sort_by ?? sortFields.map((o) => o.value)
|
36
|
+
|
37
|
+
const conf = useQuery(StoreConfigDocument).data?.storeConfig
|
38
|
+
const defaultSortBy = (
|
39
|
+
category ? category.default_sort_by ?? conf?.catalog_default_sort_by ?? 'position' : 'relevance'
|
40
|
+
) as ProductFilterParams['sort']
|
41
|
+
|
42
|
+
const formSort = useWatch({ control, name: 'sort' })
|
43
|
+
const formDirection = useWatch({ control, name: 'dir' })
|
44
|
+
const showReset = Boolean(formSort !== defaultSortBy || formDirection === 'DESC')
|
45
|
+
const selected = Boolean(params.sort && (params.sort !== defaultSortBy || params.dir === 'DESC'))
|
46
|
+
|
47
|
+
const options = useMemo(
|
48
|
+
() =>
|
49
|
+
sortFields
|
50
|
+
.filter((o) => availableSortBy.includes(o.value))
|
51
|
+
.map((option) => {
|
52
|
+
const value = option.value === defaultSortBy ? null : option.value
|
53
|
+
const showSort = formSort === value && !exclude.includes(option.value)
|
54
|
+
|
55
|
+
return {
|
56
|
+
...option,
|
57
|
+
value,
|
58
|
+
title: option.label,
|
59
|
+
...(showSort && {
|
60
|
+
onClick: () => setValue('dir', formDirection === 'DESC' ? null : 'DESC'),
|
61
|
+
price: <ProductFiltersProSortDirectionArrow sortDirection={formDirection} />,
|
62
|
+
}),
|
63
|
+
}
|
64
|
+
}),
|
65
|
+
[sortFields, availableSortBy, defaultSortBy, formSort, formDirection, setValue],
|
66
|
+
)
|
67
|
+
|
68
|
+
return {
|
69
|
+
options,
|
70
|
+
selected,
|
71
|
+
showReset,
|
72
|
+
selectedLabel: options.find((option) => option.value === params.sort)?.label,
|
73
|
+
}
|
74
|
+
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { LazyHydrate, RenderType, extendableComponent, responsiveVal } from '@graphcommerce/next-ui'
|
2
2
|
import { Box, BoxProps } from '@mui/material'
|
3
3
|
import { ProductListItemFragment } from '../../Api/ProductListItem.gql'
|
4
|
-
import { AddProductsToCartForm } from '
|
4
|
+
import { AddProductsToCartForm } from '../AddProductsToCart'
|
5
5
|
import { ProductListItemProps } from '../ProductListItem/ProductListItem'
|
6
6
|
import { ProductListItemRenderer } from './renderer'
|
7
7
|
|
@@ -31,7 +31,7 @@ export type ProductFilterParams = {
|
|
31
31
|
|
32
32
|
export function toFilterParams(params: ProductListParams): ProductFilterParams {
|
33
33
|
const [sortKey] = Object.keys(params.sort) as [keyof ProductAttributeSortInput]
|
34
|
-
const dir = params.sort[sortKey] as SortEnum | undefined
|
34
|
+
const dir = params.sort[sortKey]?.toUpperCase() as SortEnum | undefined
|
35
35
|
|
36
36
|
return {
|
37
37
|
...params,
|
@@ -35,7 +35,7 @@ export function parseParams(
|
|
35
35
|
}
|
36
36
|
if (param === 'dir') {
|
37
37
|
const [sortBy] = Object.keys(categoryVariables.sort)
|
38
|
-
if (sortBy) categoryVariables.sort[sortBy] = value as SortEnum
|
38
|
+
if (sortBy) categoryVariables.sort[sortBy] = value?.toUpperCase() as SortEnum
|
39
39
|
return undefined
|
40
40
|
}
|
41
41
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { StoreConfigQuery } from '@graphcommerce/magento-store'
|
2
|
+
import { CategoryDefaultFragment } from './CategoryDefault.gql'
|
3
|
+
import { ProductListParams } from './filterTypes'
|
4
|
+
import { cloneDeep } from '@graphcommerce/graphql'
|
5
|
+
|
6
|
+
export async function productListApplyCategoryDefaults(
|
7
|
+
params: ProductListParams | undefined,
|
8
|
+
conf: StoreConfigQuery,
|
9
|
+
category: Promise<CategoryDefaultFragment | null | undefined>,
|
10
|
+
) {
|
11
|
+
if (!params) return params
|
12
|
+
|
13
|
+
const newParams = cloneDeep(params)
|
14
|
+
if (!newParams.pageSize) newParams.pageSize = conf.storeConfig?.grid_per_page ?? 12
|
15
|
+
|
16
|
+
if (Object.keys(params.sort).length === 0) {
|
17
|
+
const categorySort = (await category)?.default_sort_by as keyof ProductListParams['sort']
|
18
|
+
const defaultSort = conf.storeConfig?.catalog_default_sort_by as keyof ProductListParams['sort']
|
19
|
+
if (categorySort) newParams.sort = { [categorySort]: 'ASC' }
|
20
|
+
else if (defaultSort) newParams.sort = { [defaultSort]: 'ASC' }
|
21
|
+
}
|
22
|
+
|
23
|
+
if (!newParams.filters.category_uid?.in?.[0]) {
|
24
|
+
newParams.filters.category_uid = { eq: (await category)?.uid }
|
25
|
+
}
|
26
|
+
|
27
|
+
return newParams
|
28
|
+
}
|
@@ -32,9 +32,11 @@ export function ProductPageBreadcrumb(props: ProductPageBreadcrumbsProps) {
|
|
32
32
|
{breadcrumb.category_name}
|
33
33
|
</Link>
|
34
34
|
))}
|
35
|
-
|
36
|
-
{category?.
|
37
|
-
|
35
|
+
{category && (
|
36
|
+
<Link href={`/${category?.url_path}`} underline='hover' color='inherit'>
|
37
|
+
{category?.name}
|
38
|
+
</Link>
|
39
|
+
)}
|
38
40
|
<Typography color='text.primary'>{name}</Typography>
|
39
41
|
</Breadcrumbs>
|
40
42
|
)
|
@@ -1,12 +1,17 @@
|
|
1
1
|
import { useWatch } from '@graphcommerce/ecommerce-ui'
|
2
2
|
import { Money } from '@graphcommerce/magento-store'
|
3
|
+
import { extendableComponent } from '@graphcommerce/next-ui'
|
4
|
+
import { Box } from '@mui/material'
|
3
5
|
import { AddToCartItemSelector, useFormAddProductsToCart } from '../AddProductsToCart'
|
4
6
|
import { ProductPagePriceFragment } from './ProductPagePrice.gql'
|
5
7
|
import { getProductTierPrice } from './getProductTierPrice'
|
6
|
-
import {
|
7
|
-
|
8
|
+
import {
|
9
|
+
UseCustomizableOptionPriceProps,
|
10
|
+
useCustomizableOptionPrice,
|
11
|
+
} from './useCustomizableOptionPrice'
|
8
12
|
|
9
|
-
export type ProductPagePriceProps = { product: ProductPagePriceFragment } & AddToCartItemSelector
|
13
|
+
export type ProductPagePriceProps = { product: ProductPagePriceFragment } & AddToCartItemSelector &
|
14
|
+
UseCustomizableOptionPriceProps
|
10
15
|
|
11
16
|
const { classes } = extendableComponent('ProductPagePrice', ['root', 'discountPrice'] as const)
|
12
17
|
|
@@ -18,6 +23,8 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
|
|
18
23
|
const price =
|
19
24
|
getProductTierPrice(product, quantity) ?? product.price_range.minimum_price.final_price
|
20
25
|
|
26
|
+
const priceValue = useCustomizableOptionPrice(props)
|
27
|
+
|
21
28
|
return (
|
22
29
|
<>
|
23
30
|
{product.price_range.minimum_price.regular_price.value !== price.value && (
|
@@ -33,7 +40,7 @@ export function ProductPagePrice(props: ProductPagePriceProps) {
|
|
33
40
|
<Money {...product.price_range.minimum_price.regular_price} />
|
34
41
|
</Box>
|
35
42
|
)}
|
36
|
-
<Money {...price} />
|
43
|
+
<Money {...price} value={priceValue} />
|
37
44
|
</>
|
38
45
|
)
|
39
46
|
}
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import { useWatch } from '@graphcommerce/ecommerce-ui'
|
2
|
+
import { PriceTypeEnum } from '@graphcommerce/graphql-mesh'
|
3
|
+
import { MoneyFragment } from '@graphcommerce/magento-store'
|
4
|
+
import { filterNonNullableKeys, isTypename, nonNullable } from '@graphcommerce/next-ui'
|
5
|
+
import type { Simplify } from 'type-fest'
|
6
|
+
import { AddToCartItemSelector, useFormAddProductsToCart } from '../AddProductsToCart'
|
7
|
+
import type { CustomizableAreaOptionFragment } from '../ProductCustomizable/CustomizableAreaOption.gql'
|
8
|
+
import type { CustomizableCheckboxOptionFragment } from '../ProductCustomizable/CustomizableCheckboxOption.gql'
|
9
|
+
import type { CustomizableDateOptionFragment } from '../ProductCustomizable/CustomizableDateOption.gql'
|
10
|
+
import type { CustomizableDropDownOptionFragment } from '../ProductCustomizable/CustomizableDropDownOption.gql'
|
11
|
+
import type { CustomizableFieldOptionFragment } from '../ProductCustomizable/CustomizableFieldOption.gql'
|
12
|
+
import type { CustomizableFileOptionFragment } from '../ProductCustomizable/CustomizableFileOption.gql'
|
13
|
+
import type { CustomizableMultipleOptionFragment } from '../ProductCustomizable/CustomizableMultipleOption.gql'
|
14
|
+
import { CustomizableRadioOptionFragment } from '../ProductCustomizable/CustomizableRadioOption.gql'
|
15
|
+
import { ProductCustomizable_SimpleProduct_Fragment } from '../ProductCustomizable/ProductCustomizable.gql'
|
16
|
+
import { ProductPagePriceFragment } from './ProductPagePrice.gql'
|
17
|
+
import { getProductTierPrice } from './getProductTierPrice'
|
18
|
+
|
19
|
+
type AnyOption = NonNullable<
|
20
|
+
NonNullable<ProductCustomizable_SimpleProduct_Fragment['options']>[number]
|
21
|
+
>
|
22
|
+
type OptionValueSelector = {
|
23
|
+
[T in AnyOption as T['__typename']]: (option: T) => Option | Option[]
|
24
|
+
}
|
25
|
+
|
26
|
+
const defaultSelectors = {
|
27
|
+
CustomizableAreaOption: (o: CustomizableAreaOptionFragment) => o.areaValue,
|
28
|
+
CustomizableCheckboxOption: (o: CustomizableCheckboxOptionFragment) => o.checkboxValue,
|
29
|
+
CustomizableFileOption: (o: CustomizableFileOptionFragment) => o.fileValue,
|
30
|
+
CustomizableDateOption: (o: CustomizableDateOptionFragment) => o.dateValue,
|
31
|
+
CustomizableDropDownOption: (o: CustomizableDropDownOptionFragment) => o.dropdownValue,
|
32
|
+
CustomizableFieldOption: (o: CustomizableFieldOptionFragment) => o.fieldValue,
|
33
|
+
CustomizableMultipleOption: (o: CustomizableMultipleOptionFragment) => o.multipleValue,
|
34
|
+
CustomizableRadioOption: (o: CustomizableRadioOptionFragment) => o.radioValue,
|
35
|
+
}
|
36
|
+
|
37
|
+
type MissingOptionValueSelectors = Omit<OptionValueSelector, keyof typeof defaultSelectors>
|
38
|
+
type DefinedOptionValueSelectors = Partial<Pick<OptionValueSelector, keyof typeof defaultSelectors>>
|
39
|
+
|
40
|
+
type Selectors = Simplify<
|
41
|
+
keyof MissingOptionValueSelectors extends never
|
42
|
+
? (MissingOptionValueSelectors & DefinedOptionValueSelectors) | undefined
|
43
|
+
: MissingOptionValueSelectors & DefinedOptionValueSelectors
|
44
|
+
>
|
45
|
+
|
46
|
+
export type UseCustomizableOptionPriceProps = {
|
47
|
+
product: ProductPagePriceFragment
|
48
|
+
} & AddToCartItemSelector &
|
49
|
+
(keyof MissingOptionValueSelectors extends never
|
50
|
+
? { selectors?: Selectors }
|
51
|
+
: { selectors: Selectors })
|
52
|
+
|
53
|
+
type Option =
|
54
|
+
| {
|
55
|
+
price?: number | null | undefined
|
56
|
+
price_type?: PriceTypeEnum | null | undefined
|
57
|
+
uid?: string | null | undefined
|
58
|
+
}
|
59
|
+
| undefined
|
60
|
+
| null
|
61
|
+
|
62
|
+
function calcOptionPrice(option: Option, product: MoneyFragment) {
|
63
|
+
if (!option?.price) return 0
|
64
|
+
switch (option.price_type) {
|
65
|
+
case 'DYNAMIC':
|
66
|
+
case 'FIXED':
|
67
|
+
return option.price
|
68
|
+
case 'PERCENT':
|
69
|
+
return (product?.value ?? 0) * (option.price / 100)
|
70
|
+
}
|
71
|
+
|
72
|
+
return 0
|
73
|
+
}
|
74
|
+
|
75
|
+
export function useCustomizableOptionPrice(props: UseCustomizableOptionPriceProps) {
|
76
|
+
const { product, selectors, index = 0 } = props
|
77
|
+
|
78
|
+
const { control } = useFormAddProductsToCart()
|
79
|
+
const cartItem = useWatch({ control, name: `cartItems.${index}` }) ?? {}
|
80
|
+
const price =
|
81
|
+
getProductTierPrice(product, cartItem?.quantity) ??
|
82
|
+
product.price_range.minimum_price.final_price
|
83
|
+
|
84
|
+
const allSelectors: OptionValueSelector = { ...defaultSelectors, ...selectors }
|
85
|
+
|
86
|
+
if (isTypename(product, ['GroupedProduct'])) return price.value
|
87
|
+
if (!product.options || product.options.length === 0) return price.value
|
88
|
+
|
89
|
+
const finalPrice = product.options.filter(nonNullable).reduce((optionPrice, productOption) => {
|
90
|
+
const isCustomizable = Boolean(cartItem.customizable_options?.[productOption.uid])
|
91
|
+
const isEntered = Boolean(
|
92
|
+
cartItem.entered_options?.find((o) => productOption.uid && o?.uid && o?.value),
|
93
|
+
)
|
94
|
+
if (!isCustomizable && !isEntered) return optionPrice
|
95
|
+
|
96
|
+
const selector = allSelectors[productOption.__typename] as
|
97
|
+
| undefined
|
98
|
+
| ((option: AnyOption) => Option | Option[])
|
99
|
+
const value = selector ? selector(productOption) : null
|
100
|
+
|
101
|
+
if (!value) return 0
|
102
|
+
|
103
|
+
// If the option can have multiple values
|
104
|
+
if (Array.isArray(value)) {
|
105
|
+
return (
|
106
|
+
optionPrice +
|
107
|
+
filterNonNullableKeys(value)
|
108
|
+
.filter(
|
109
|
+
(v) =>
|
110
|
+
cartItem.customizable_options?.[productOption.uid] &&
|
111
|
+
cartItem.customizable_options?.[productOption.uid].includes(v.uid),
|
112
|
+
)
|
113
|
+
.reduce((p, v) => p + calcOptionPrice(v, price), 0)
|
114
|
+
)
|
115
|
+
}
|
116
|
+
|
117
|
+
// If the option can have a single value entered.
|
118
|
+
if (
|
119
|
+
cartItem.entered_options?.filter((v) => v?.uid === productOption.uid && v.value).length !== 0
|
120
|
+
)
|
121
|
+
return optionPrice + calcOptionPrice(value, price)
|
122
|
+
|
123
|
+
return optionPrice
|
124
|
+
}, price.value ?? 0)
|
125
|
+
|
126
|
+
return finalPrice
|
127
|
+
}
|