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