@graphcommerce/magento-search 8.1.0-canary.9 → 9.0.0-canary.101

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 CHANGED
@@ -1,5 +1,201 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.0.0-canary.101
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2380](https://github.com/graphcommerce-org/graphcommerce/pull/2380) [`c17e5f1`](https://github.com/graphcommerce-org/graphcommerce/commit/c17e5f1cf9fb291b9bbf1fca0620c2721dceb331) - Solve issue: Warning: Cannot update a component (`FormAutoSubmitBase`) while rendering a different component (`ActionCardListForm`). ([@paales](https://github.com/paales))
8
+
9
+ ## 9.0.0-canary.100
10
+
11
+ ## 9.0.0-canary.99
12
+
13
+ ## 9.0.0-canary.98
14
+
15
+ ## 9.0.0-canary.97
16
+
17
+ ## 9.0.0-canary.96
18
+
19
+ ## 9.0.0-canary.95
20
+
21
+ ## 9.0.0-canary.94
22
+
23
+ ## 9.0.0-canary.93
24
+
25
+ ## 9.0.0-canary.92
26
+
27
+ ## 9.0.0-canary.91
28
+
29
+ ## 9.0.0-canary.90
30
+
31
+ ## 9.0.0-canary.89
32
+
33
+ ## 9.0.0-canary.88
34
+
35
+ ## 9.0.0-canary.87
36
+
37
+ ## 9.0.0-canary.86
38
+
39
+ ## 9.0.0-canary.85
40
+
41
+ ## 9.0.0-canary.84
42
+
43
+ ## 9.0.0-canary.83
44
+
45
+ ## 9.0.0-canary.82
46
+
47
+ ## 9.0.0-canary.81
48
+
49
+ ## 9.0.0-canary.80
50
+
51
+ ## 9.0.0-canary.79
52
+
53
+ ## 9.0.0-canary.78
54
+
55
+ ## 9.0.0-canary.77
56
+
57
+ ## 9.0.0-canary.76
58
+
59
+ ## 9.0.0-canary.75
60
+
61
+ ## 9.0.0-canary.74
62
+
63
+ ## 9.0.0-canary.73
64
+
65
+ ## 9.0.0-canary.72
66
+
67
+ ## 9.0.0-canary.71
68
+
69
+ ## 9.0.0-canary.70
70
+
71
+ ## 9.0.0-canary.69
72
+
73
+ ## 9.0.0-canary.68
74
+
75
+ ## 9.0.0-canary.67
76
+
77
+ ## 9.0.0-canary.66
78
+
79
+ ## 9.0.0-canary.65
80
+
81
+ ## 9.0.0-canary.64
82
+
83
+ ## 9.0.0-canary.63
84
+
85
+ ## 9.0.0-canary.62
86
+
87
+ ## 9.0.0-canary.61
88
+
89
+ ## 9.0.0-canary.60
90
+
91
+ ## 9.0.0-canary.59
92
+
93
+ ### Patch Changes
94
+
95
+ - [#2309](https://github.com/graphcommerce-org/graphcommerce/pull/2309) [`03e410f`](https://github.com/graphcommerce-org/graphcommerce/commit/03e410f7ad59ce94a6ff199809999e56ff8cc1f5) - Solve issue where the input field wouldn't open and wouldn't be focussed on render. ([@Renzovh](https://github.com/Renzovh))
96
+
97
+ ## 9.0.0-canary.58
98
+
99
+ ### Patch Changes
100
+
101
+ - [#2328](https://github.com/graphcommerce-org/graphcommerce/pull/2328) [`ee04368`](https://github.com/graphcommerce-org/graphcommerce/commit/ee04368444f732e5541a595db6e2ef66d15add68) - Move to attributesList to get a list of filterable attributes instead of using an introspection query. `productFiltersProSectionRenderer` and `productFiltersProChipRenderer` keys now now one of `AttributeFrontendInputEnum`. ([@paales](https://github.com/paales))
102
+
103
+ ## 9.0.0-canary.57
104
+
105
+ ## 9.0.0-canary.56
106
+
107
+ ## 9.0.0-canary.55
108
+
109
+ ## 9.0.0-canary.54
110
+
111
+ ## 8.1.0-canary.53
112
+
113
+ ## 8.1.0-canary.52
114
+
115
+ ## 8.1.0-canary.51
116
+
117
+ ## 8.1.0-canary.50
118
+
119
+ ## 8.1.0-canary.49
120
+
121
+ ## 8.1.0-canary.48
122
+
123
+ ## 8.1.0-canary.47
124
+
125
+ ## 8.1.0-canary.46
126
+
127
+ ## 8.1.0-canary.45
128
+
129
+ ## 8.1.0-canary.44
130
+
131
+ ## 8.1.0-canary.43
132
+
133
+ ## 8.1.0-canary.42
134
+
135
+ ## 8.1.0-canary.41
136
+
137
+ ## 8.1.0-canary.40
138
+
139
+ ## 8.1.0-canary.39
140
+
141
+ ## 8.1.0-canary.38
142
+
143
+ ## 8.1.0-canary.37
144
+
145
+ ## 8.1.0-canary.36
146
+
147
+ ## 8.1.0-canary.35
148
+
149
+ ## 8.1.0-canary.34
150
+
151
+ ## 8.1.0-canary.33
152
+
153
+ ## 8.1.0-canary.32
154
+
155
+ ## 8.1.0-canary.31
156
+
157
+ ## 8.1.0-canary.30
158
+
159
+ ## 8.1.0-canary.29
160
+
161
+ ## 8.1.0-canary.28
162
+
163
+ ## 8.1.0-canary.27
164
+
165
+ ## 8.1.0-canary.26
166
+
167
+ ## 8.1.0-canary.25
168
+
169
+ ## 8.1.0-canary.24
170
+
171
+ ## 8.1.0-canary.23
172
+
173
+ ## 8.1.0-canary.22
174
+
175
+ ## 8.1.0-canary.21
176
+
177
+ ## 8.1.0-canary.20
178
+
179
+ ## 8.1.0-canary.19
180
+
181
+ ## 8.1.0-canary.18
182
+
183
+ ## 8.1.0-canary.17
184
+
185
+ ## 8.1.0-canary.16
186
+
187
+ ## 8.1.0-canary.15
188
+
189
+ ## 8.1.0-canary.14
190
+
191
+ ## 8.1.0-canary.13
192
+
193
+ ## 8.1.0-canary.12
194
+
195
+ ## 8.1.0-canary.11
196
+
197
+ ## 8.1.0-canary.10
198
+
3
199
  ## 8.1.0-canary.9
4
200
 
5
201
  ## 8.1.0-canary.8
@@ -92,14 +288,11 @@
92
288
 
93
289
  ### Patch Changes
94
290
 
95
- - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`9091dbb`](https://github.com/graphcommerce-org/graphcommerce/commit/9091dbb01a47aa6ba40932a66ed055b7f07c1cec) - Make sure the search link in the header is a soft navigation instead of a hard browser navigation.
96
- ([@paales](https://github.com/paales))
291
+ - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`9091dbb`](https://github.com/graphcommerce-org/graphcommerce/commit/9091dbb01a47aa6ba40932a66ed055b7f07c1cec) - Make sure the search link in the header is a soft navigation instead of a hard browser navigation. ([@paales](https://github.com/paales))
97
292
 
98
- - [`e33660f`](https://github.com/graphcommerce-org/graphcommerce/commit/e33660f172466dcfa0ab7262cee612d9a3e47776) - Accessibility improvements for the frontend: Added skip content link. Removed empty buttons from tab flow. Gave focus to elements (such as the menu) that appear when after clicking a button. Improved aria labels where needed
99
- ([@FrankHarland](https://github.com/FrankHarland))
293
+ - [`e33660f`](https://github.com/graphcommerce-org/graphcommerce/commit/e33660f172466dcfa0ab7262cee612d9a3e47776) - Accessibility improvements for the frontend: Added skip content link. Removed empty buttons from tab flow. Gave focus to elements (such as the menu) that appear when after clicking a button. Improved aria labels where needed ([@FrankHarland](https://github.com/FrankHarland))
100
294
 
101
- - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`fe37229`](https://github.com/graphcommerce-org/graphcommerce/commit/fe372294d6a42b1108e0fcef306b297baed5eb71) - Take the `per_page` configuration in account for the search results
102
- ([@paales](https://github.com/paales))
295
+ - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`fe37229`](https://github.com/graphcommerce-org/graphcommerce/commit/fe372294d6a42b1108e0fcef306b297baed5eb71) - Take the `per_page` configuration in account for the search results ([@paales](https://github.com/paales))
103
296
 
104
297
  ## 8.0.0-canary.100
105
298
 
@@ -151,8 +344,7 @@
151
344
 
152
345
  ### Patch Changes
153
346
 
154
- - [`e33660f`](https://github.com/graphcommerce-org/graphcommerce/commit/e33660f172466dcfa0ab7262cee612d9a3e47776) - a11y improvements (see https://github.com/graphcommerce-org/graphcommerce/issues/1995 for more info)
155
- ([@FrankHarland](https://github.com/FrankHarland))
347
+ - [`e33660f`](https://github.com/graphcommerce-org/graphcommerce/commit/e33660f172466dcfa0ab7262cee612d9a3e47776) - a11y improvements (see https://github.com/graphcommerce-org/graphcommerce/issues/1995 for more info) ([@FrankHarland](https://github.com/FrankHarland))
156
348
 
157
349
  ## 8.0.0-canary.76
158
350
 
@@ -160,11 +352,9 @@
160
352
 
161
353
  ### Patch Changes
162
354
 
163
- - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`9091dbb`](https://github.com/graphcommerce-org/graphcommerce/commit/9091dbb01a47aa6ba40932a66ed055b7f07c1cec) - Make sure the search link in the header is a nextjs navigation
164
- ([@paales](https://github.com/paales))
355
+ - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`9091dbb`](https://github.com/graphcommerce-org/graphcommerce/commit/9091dbb01a47aa6ba40932a66ed055b7f07c1cec) - Make sure the search link in the header is a nextjs navigation ([@paales](https://github.com/paales))
165
356
 
166
- - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`fe37229`](https://github.com/graphcommerce-org/graphcommerce/commit/fe372294d6a42b1108e0fcef306b297baed5eb71) - Take the per_page configuration in account for the search results
167
- ([@paales](https://github.com/paales))
357
+ - [#2160](https://github.com/graphcommerce-org/graphcommerce/pull/2160) [`fe37229`](https://github.com/graphcommerce-org/graphcommerce/commit/fe372294d6a42b1108e0fcef306b297baed5eb71) - Take the per_page configuration in account for the search results ([@paales](https://github.com/paales))
168
358
 
169
359
  ## 8.0.0-canary.74
170
360
 
@@ -1217,31 +1407,31 @@
1217
1407
  All occurences of `<Trans>` and `t` need to be replaced:
1218
1408
 
1219
1409
  ```tsx
1220
- import { Trans, t } from "@lingui/macro";
1410
+ import { Trans, t } from '@lingui/macro'
1221
1411
 
1222
1412
  function MyComponent() {
1223
- const foo = "bar";
1413
+ const foo = 'bar'
1224
1414
  return (
1225
1415
  <div aria-label={t`Account ${foo}`}>
1226
1416
  <Trans>My Translation {foo}</Trans>
1227
1417
  </div>
1228
- );
1418
+ )
1229
1419
  }
1230
1420
  ```
1231
1421
 
1232
1422
  Needs to be replaced with:
1233
1423
 
1234
1424
  ```tsx
1235
- import { Trans } from "@lingui/react";
1236
- import { i18n } from "@lingui/core";
1425
+ import { Trans } from '@lingui/react'
1426
+ import { i18n } from '@lingui/core'
1237
1427
 
1238
1428
  function MyComponent() {
1239
- const foo = "bar";
1429
+ const foo = 'bar'
1240
1430
  return (
1241
1431
  <div aria-label={i18n._(/* i18n */ `Account {foo}`, { foo })}>
1242
- <Trans key="My Translation {foo}" values={{ foo }}></Trans>
1432
+ <Trans key='My Translation {foo}' values={{ foo }}></Trans>
1243
1433
  </div>
1244
- );
1434
+ )
1245
1435
  }
1246
1436
  ```
1247
1437
 
@@ -2,16 +2,14 @@ import { extendableComponent } from '@graphcommerce/next-ui'
2
2
  import { Trans } from '@lingui/react'
3
3
  import { Box, SxProps, Theme, Typography } from '@mui/material'
4
4
 
5
- export type NoSearchResultsProps = { search: string; sx?: SxProps<Theme> }
5
+ export type NoSearchResultsProps = { sx?: SxProps<Theme> }
6
6
 
7
7
  const name = 'NoSearchResults' as const
8
8
  const parts = ['root'] as const
9
9
  const { classes } = extendableComponent(name, parts)
10
10
 
11
11
  export function NoSearchResults(props: NoSearchResultsProps) {
12
- const { search, sx = [] } = props
13
-
14
- const term = `'${search}'`
12
+ const { sx = [] } = props
15
13
 
16
14
  return (
17
15
  <Box
@@ -26,7 +24,7 @@ export function NoSearchResults(props: NoSearchResultsProps) {
26
24
  ]}
27
25
  >
28
26
  <Typography variant='h5' align='center'>
29
- <Trans id="We couldn't find any results for {term}" values={{ term }} />
27
+ <Trans id="We couldn't find any products." />
30
28
  </Typography>
31
29
  <p>
32
30
  <Trans id='Try a different search' />
@@ -0,0 +1,170 @@
1
+ import type {
2
+ MenuQueryFragment,
3
+ CategoryTreeItem,
4
+ NavigationItemFragment,
5
+ } from '@graphcommerce/magento-category'
6
+ import {
7
+ ProductFiltersProCategoryAccordion,
8
+ ProductFiltersProCategoryAccordionProps,
9
+ useProductFiltersPro,
10
+ } from '@graphcommerce/magento-product'
11
+ import { filterNonNullableKeys } from '@graphcommerce/next-ui'
12
+ import { useMemo } from 'react'
13
+
14
+ type MenuItem = NavigationItemFragment & {
15
+ children?: Array<MenuItem | null | undefined> | null | undefined
16
+ }
17
+
18
+ type TreeItem = NavigationItemFragment & {
19
+ visible?: boolean
20
+ parent: TreeItem | undefined
21
+ children: TreeItem[]
22
+ }
23
+
24
+ function menuItemToTreeItem(item: MenuItem, parent: TreeItem | undefined): TreeItem {
25
+ const newItem: TreeItem = { ...item, parent, children: [] }
26
+ newItem.children = filterNonNullableKeys(item.children).map((child) =>
27
+ menuItemToTreeItem(child, newItem),
28
+ )
29
+ return newItem
30
+ }
31
+
32
+ function treeFind<U extends TreeItem>(tree: U, fn: (item: U) => boolean): U | undefined {
33
+ if (fn(tree)) return tree
34
+ for (const child of tree.children ?? []) {
35
+ const found = treeFind<U>(child as U, fn)
36
+ if (found) return found
37
+ }
38
+ return undefined
39
+ }
40
+
41
+ function treeFlatMap<U extends TreeItem, R>(
42
+ tree: U | undefined,
43
+ cb: (item: U, level: number) => R,
44
+ _level = 0,
45
+ ): R[] {
46
+ if (!tree) return []
47
+
48
+ const mapped = cb(tree, _level)
49
+ const children = tree.children.flatMap((child) => treeFlatMap(child as U, cb, _level + 1))
50
+ return [mapped, ...children]
51
+ }
52
+
53
+ function treeWalkFilter<U extends TreeItem>(
54
+ treeItem: U,
55
+ fn: (newTreeItem: U) => boolean,
56
+ ): U | undefined {
57
+ const children = treeItem.children.map((child) => treeWalkFilter(child as U, fn)).filter(Boolean)
58
+ const newTreeItem = { ...treeItem, children }
59
+ return children.length > 0 || fn(newTreeItem) ? newTreeItem : undefined
60
+ }
61
+
62
+ function treeWalk<U extends TreeItem>(root: U | undefined, fn: (item: U) => void) {
63
+ if (!root) return
64
+ root.children.map((child) => treeWalk(child as U, fn))
65
+ fn(root)
66
+ }
67
+
68
+ function allParents<U extends TreeItem>(item: U): U[] {
69
+ const parents = item.parent ? [item.parent, ...allParents(item.parent)] : []
70
+ return parents as U[]
71
+ }
72
+
73
+ function isParent<U extends TreeItem>(item: U, parent: U): boolean {
74
+ let p = parent.parent
75
+ while (p) {
76
+ if (p.uid === item.uid) return true
77
+ p = p.parent
78
+ }
79
+ return false
80
+ }
81
+
82
+ type ProductFiltersProCategorySectionSearchProps = Omit<
83
+ ProductFiltersProCategoryAccordionProps,
84
+ 'categoryTree' | 'onChange'
85
+ > & {
86
+ menu?: MenuQueryFragment['menu']
87
+ }
88
+
89
+ export function ProductFiltersProCategorySectionSearch(
90
+ props: ProductFiltersProCategorySectionSearchProps,
91
+ ) {
92
+ const { menu } = props
93
+ const { form, submit, params, aggregations, appliedAggregations } = useProductFiltersPro()
94
+ const currentFilter = params.filters.category_uid?.in
95
+
96
+ const categoryTree = useMemo(() => {
97
+ const rootCategory = menu?.items?.[0]
98
+ if (!rootCategory) return []
99
+
100
+ let tree: TreeItem | undefined = menuItemToTreeItem(rootCategory, undefined)
101
+
102
+ const currentCounts = aggregations?.find((a) => a?.attribute_code === 'category_uid')?.options
103
+
104
+ const activeItem = treeFind(tree, (item) => currentFilter?.includes(item.uid) ?? false) ?? tree
105
+
106
+ // Mark all parents as visible if they have a count.
107
+ treeWalk(tree, (item) => {
108
+ const count = currentCounts?.find((i) => item.uid === i?.value)?.count ?? null
109
+ if (!count) return
110
+
111
+ item.visible = true
112
+ allParents(item).forEach((p) => {
113
+ p.visible = true
114
+ })
115
+ })
116
+
117
+ tree = treeWalkFilter(tree, (item) => {
118
+ // If currently active
119
+ if (activeItem.uid === item.uid) return true
120
+
121
+ if (!item.include_in_menu) return false
122
+
123
+ // Show direct children of active item.
124
+ if (activeItem.uid === item.parent?.uid) return true
125
+
126
+ // Show siblings if there are are only a few children.
127
+ if (activeItem.children.length <= 5 && item.parent?.uid === activeItem.parent?.uid)
128
+ return true
129
+
130
+ return false
131
+ })
132
+
133
+ // Als een child een count heeft, dan alle parents ook een count geven
134
+
135
+ return treeFlatMap<TreeItem, CategoryTreeItem>(tree, (item, level) => {
136
+ const count = currentCounts?.find((i) => item.uid === i?.value)?.count ?? null
137
+
138
+ return {
139
+ uid: item.uid,
140
+ title: item.name,
141
+ value: item.url_path ?? '',
142
+ selected: currentFilter?.includes(item.uid) ?? false,
143
+ indent: level - 1,
144
+ count,
145
+ isBack: isParent(item, activeItem),
146
+ visible: item.visible,
147
+ }
148
+ })
149
+ .slice(1)
150
+ .filter((c) => c.visible)
151
+ }, [aggregations, currentFilter, menu?.items])
152
+
153
+ if (!categoryTree.length) return null
154
+
155
+ return (
156
+ <ProductFiltersProCategoryAccordion
157
+ categoryTree={categoryTree}
158
+ {...props}
159
+ onChange={async (item) => {
160
+ form.setValue('filters', {
161
+ category_uid: {
162
+ in: item.uid === currentFilter?.[0] ? null : [item?.uid],
163
+ },
164
+ })
165
+
166
+ await submit()
167
+ }}
168
+ />
169
+ )
170
+ }
@@ -0,0 +1,62 @@
1
+ import { IconSvg, iconSearch, showPageLoadIndicator } from '@graphcommerce/next-ui'
2
+ import { Fab, FabProps } from '@mui/material'
3
+ import dynamic from 'next/dynamic'
4
+ import { useMemo, useState } from 'react'
5
+ import { ProductFiltersProSearchInputProps } from './ProductFiltersProSearchInput'
6
+ import { useSearchPageAndParam } from './useSearchPageAndParam'
7
+
8
+ type ProductFiltersProSearchFieldProps = ProductFiltersProSearchInputProps & {
9
+ fab?: FabProps
10
+ }
11
+
12
+ const ProductFiltersProSearchInputLazy = dynamic(
13
+ async () => (await import('./ProductFiltersProSearchInput')).ProductFiltersProSearchOutlinedInput,
14
+ )
15
+
16
+ export function ProductFiltersProSearchField(props: ProductFiltersProSearchFieldProps) {
17
+ const { fab, formControl } = props
18
+
19
+ const [searchPage] = useSearchPageAndParam()
20
+ const [expanded, setExpanded] = useState(searchPage)
21
+ useMemo(() => {
22
+ if (!searchPage) setExpanded(searchPage)
23
+ }, [searchPage])
24
+
25
+ const visible = expanded || searchPage
26
+
27
+ return (
28
+ <>
29
+ {visible && (
30
+ <ProductFiltersProSearchInputLazy
31
+ {...props}
32
+ formControl={formControl}
33
+ inputRef={(element: HTMLInputElement) => element?.focus()}
34
+ // autoFocus
35
+ buttonProps={{
36
+ onClick: () => {
37
+ setExpanded(false)
38
+ },
39
+ }}
40
+ onBlur={() => {
41
+ if (!searchPage && !showPageLoadIndicator.get()) setExpanded(false)
42
+ }}
43
+ />
44
+ )}
45
+ <Fab
46
+ onClick={() => {
47
+ setExpanded(true)
48
+ // inputRef.current?.focus()
49
+ }}
50
+ color='inherit'
51
+ size='large'
52
+ {...fab}
53
+ sx={[
54
+ { display: { xs: visible ? 'none' : 'inline-flex' } },
55
+ ...(Array.isArray(fab?.sx) ? fab.sx : [fab?.sx]),
56
+ ]}
57
+ >
58
+ <IconSvg src={iconSearch} size='large' />
59
+ </Fab>
60
+ </>
61
+ )
62
+ }
@@ -0,0 +1,36 @@
1
+ import { ProductListParams, useProductFiltersPro } from '@graphcommerce/magento-product'
2
+ import { useWatch } from '@graphcommerce/react-hook-form'
3
+ import { Trans } from '@lingui/macro'
4
+ import { Box } from '@mui/material'
5
+
6
+ type ProductFiltersProSearchHeaderProps = {
7
+ params: ProductListParams
8
+ /**
9
+ * Provide a text when there is no term searched
10
+ */
11
+ children: React.ReactNode
12
+ }
13
+
14
+ export function ProductFiltersProSearchTerm(props: ProductFiltersProSearchHeaderProps) {
15
+ const { params, children } = props
16
+ const { form } = useProductFiltersPro()
17
+ const resultSearch = params.search ?? ''
18
+ const targetSearch = useWatch({ control: form.control, name: 'search' }) ?? ''
19
+
20
+ const remaining = targetSearch.startsWith(resultSearch)
21
+ ? targetSearch.slice(resultSearch.length)
22
+ : ''
23
+
24
+ if (!resultSearch && !targetSearch) return children
25
+
26
+ const search = (
27
+ <>
28
+ <Box component='span'>{resultSearch}</Box>
29
+ <Box component='span' sx={{ color: 'text.disabled' }}>
30
+ {remaining}
31
+ </Box>
32
+ </>
33
+ )
34
+
35
+ return <Trans>Results for ‘{search}’</Trans>
36
+ }
@@ -0,0 +1,123 @@
1
+ import { globalFormContextRef } from '@graphcommerce/magento-product'
2
+ import { IconSvg, iconClose } from '@graphcommerce/next-ui'
3
+ import { t } from '@lingui/macro'
4
+ import {
5
+ ButtonBaseProps,
6
+ FormControl,
7
+ FormControlProps,
8
+ IconButton,
9
+ IconButtonProps,
10
+ InputBaseProps,
11
+ OutlinedInput,
12
+ OutlinedInputProps,
13
+ useForkRef,
14
+ } from '@mui/material'
15
+ import { useRouter } from 'next/router'
16
+ import { useEffect, useRef } from 'react'
17
+ import { useSearchPageAndParam } from './useSearchPageAndParam'
18
+
19
+ export function useProductFiltersProSearchInput<
20
+ P extends InputBaseProps & { buttonProps?: ButtonBaseProps },
21
+ >(props: P): P {
22
+ const { buttonProps = {}, inputRef } = props
23
+
24
+ const router = useRouter()
25
+ const [searchPage, searchParam] = useSearchPageAndParam()
26
+
27
+ const internalRef = useRef<HTMLInputElement>(null)
28
+ const ref = useForkRef(inputRef, internalRef)
29
+ const initial = useRef(true)
30
+
31
+ useEffect(() => {
32
+ // When page initially loads, fill in the search field with the search param.
33
+ if (internalRef.current && initial.current && searchParam) {
34
+ initial.current = false
35
+ internalRef.current.selectionStart = searchParam.length
36
+ internalRef.current.selectionEnd = searchParam.length
37
+ return
38
+ }
39
+
40
+ // When the user is not focussed on the search field and the value gets updated, update the form.
41
+ if (internalRef.current && internalRef.current !== document.activeElement && searchParam)
42
+ internalRef.current.value = searchParam
43
+ }, [searchParam])
44
+
45
+ const result: P = {
46
+ ...props,
47
+ inputRef: ref,
48
+ placeholder: t`Search all products...`,
49
+ name: 'search',
50
+ type: 'text',
51
+ defaultValue: searchParam,
52
+ onKeyDown: (e) => {
53
+ if (e.key === 'Enter') {
54
+ const context = globalFormContextRef.current
55
+ if (!context || !searchPage) {
56
+ return router.push(`/search/${e.currentTarget.value}`)
57
+ }
58
+ context.form.setValue('currentPage', 1)
59
+ context.form.setValue('search', e.currentTarget.value)
60
+ return context.submit()
61
+ }
62
+ return props?.onKeyDown?.(e)
63
+ },
64
+ onChange: async (e) => {
65
+ const context = globalFormContextRef.current
66
+
67
+ // When we're not on the search page, we want to navigate as soon as possible.
68
+ // TODO: We only want to navigate once, and let the rest be handled by the search page.
69
+ if (!context || !searchPage) {
70
+ return router.push(`/search/${e.target.value}`)
71
+ }
72
+
73
+ context.form.setValue('currentPage', 1)
74
+ context.form.setValue('search', e.currentTarget.value)
75
+ await context.submit()
76
+
77
+ return props.onChange?.(e)
78
+ },
79
+ buttonProps: {
80
+ ...buttonProps,
81
+ onClick: async (e) => {
82
+ const context = globalFormContextRef.current
83
+
84
+ if (context?.form.getValues('search')) {
85
+ context.form.setValue('currentPage', 1)
86
+ context.form.setValue('search', '')
87
+ if (internalRef.current) internalRef.current.value = ''
88
+ await context.submit()
89
+ } else if (searchPage) {
90
+ router.back()
91
+ if (internalRef.current) internalRef.current.value = ''
92
+ } else {
93
+ buttonProps.onClick?.(e)
94
+ }
95
+ },
96
+ },
97
+ }
98
+ return result
99
+ }
100
+
101
+ export type ProductFiltersProSearchInputProps = OutlinedInputProps & {
102
+ formControl?: FormControlProps
103
+ buttonProps?: IconButtonProps
104
+ }
105
+
106
+ export function ProductFiltersProSearchOutlinedInput(props: ProductFiltersProSearchInputProps) {
107
+ const { buttonProps, formControl, size, ...rest } = useProductFiltersProSearchInput(props)
108
+
109
+ return (
110
+ <FormControl variant='outlined' size={size} {...formControl}>
111
+ <OutlinedInput
112
+ color='primary'
113
+ size={size}
114
+ endAdornment={
115
+ <IconButton color='inherit' size='small' {...buttonProps}>
116
+ <IconSvg src={iconClose} size='large' />
117
+ </IconButton>
118
+ }
119
+ {...rest}
120
+ />
121
+ </FormControl>
122
+ )
123
+ }
@@ -0,0 +1,12 @@
1
+ import { extractUrlQuery } from '@graphcommerce/magento-product'
2
+ import { useRouter } from 'next/router'
3
+
4
+ export function useSearchPageAndParam() {
5
+ const router = useRouter()
6
+
7
+ const path = router.asPath.startsWith('/c/') ? router.asPath.slice(3) : router.asPath.slice(1)
8
+ const [url, query] = extractUrlQuery({ url: path.split('#')[0].split('/') })
9
+ const searchParam = url?.startsWith('search') ? decodeURI(url.split('/')[1] ?? '') : null
10
+ const searchPage = router.asPath.startsWith('/search')
11
+ return [searchPage, searchParam] as const
12
+ }
@@ -29,7 +29,9 @@ export function SearchForm(props: SearchFormProps) {
29
29
  const form = useForm({ defaultValues: { search } })
30
30
  const { handleSubmit, setValue, control } = form
31
31
 
32
- const submit = handleSubmit((formData) => router.replace(`/${urlHandle}/${formData.search}`))
32
+ const submit = handleSubmit((formData) =>
33
+ router.replace(`/${urlHandle}/${formData.search}`, undefined, { shallow: true }),
34
+ )
33
35
 
34
36
  const endAdornment = (
35
37
  <SearchFormAdornment
@@ -0,0 +1,66 @@
1
+ import { useInContextQuery, useQuery } from '@graphcommerce/graphql'
2
+ import {
3
+ FilterFormProviderProps,
4
+ ProductFiltersDocument,
5
+ ProductFiltersQuery,
6
+ ProductListDocument,
7
+ ProductListParams,
8
+ ProductListQuery,
9
+ prefetchProductList,
10
+ toProductListParams,
11
+ useRouterFilterParams,
12
+ } from '@graphcommerce/magento-product'
13
+ import { StoreConfigDocument } from '@graphcommerce/magento-store'
14
+ import { useEventCallback } from '@mui/material'
15
+ import {
16
+ productListApplySearchDefaults,
17
+ searchDefaultsToProductListFilters,
18
+ useProductListApplySearchDefaults,
19
+ } from '../utils/productListApplySearchDefaults'
20
+
21
+ /**
22
+ * - Handles shallow routing requests
23
+ * - Handles customer specific product list queries
24
+ * - Creates a prefetch function to preload the product list
25
+ */
26
+ export function useProductList<
27
+ T extends ProductListQuery &
28
+ ProductFiltersQuery & {
29
+ params?: ProductListParams
30
+ },
31
+ >(props: T) {
32
+ const { params, shallow } = useRouterFilterParams(props)
33
+ const variables = useProductListApplySearchDefaults(params)
34
+ const result = useInContextQuery(ProductListDocument, { variables, skip: !shallow }, props)
35
+ const filters = useInContextQuery(
36
+ ProductFiltersDocument,
37
+ { variables: searchDefaultsToProductListFilters(variables), skip: !shallow },
38
+ props,
39
+ )
40
+
41
+ const storeConfig = useQuery(StoreConfigDocument).data
42
+
43
+ const handleSubmit: NonNullable<FilterFormProviderProps['handleSubmit']> = useEventCallback(
44
+ async (formValues, next) => {
45
+ if (!storeConfig) return
46
+
47
+ const vars = productListApplySearchDefaults(toProductListParams(formValues), storeConfig)
48
+ await prefetchProductList(
49
+ vars,
50
+ searchDefaultsToProductListFilters(vars),
51
+ next,
52
+ result.client,
53
+ true,
54
+ )
55
+ },
56
+ )
57
+
58
+ return {
59
+ ...props,
60
+ filters: filters.data.filters,
61
+ ...result.data,
62
+ params,
63
+ mask: result.mask,
64
+ handleSubmit,
65
+ }
66
+ }
package/index.ts CHANGED
@@ -1,22 +1,26 @@
1
- export * from './components/SearchDivider/SearchDivider'
2
- export * from './components/NoSearchResults/NoSearchResults'
1
+ export * from './CategorySearch.gql'
3
2
  export * from './components/CategorySearchResult/CategorySearchResult'
4
3
  export * from './components/CategorySearchResult/CategorySearchResults'
5
- export * from './components/SearchForm/SearchForm'
6
- export * from './components/SearchLink/SearchLink'
7
- export * from './CategorySearch.gql'
4
+ export * from './components/NoSearchResults/NoSearchResults'
8
5
  export * from './components/SearchButton/SearchButton'
9
6
  export * from './components/SearchContext/SearchContext'
7
+ export * from './components/SearchDivider/SearchDivider'
8
+ export * from './components/SearchForm/SearchForm'
9
+ export * from './components/SearchLink/SearchLink'
10
+ export * from './hooks/useProductList'
11
+ export * from './components/ProductFiltersPro/ProductFiltersProCategorySectionSearch'
12
+ export * from './components/ProductFiltersPro/ProductFiltersProSearchHeader'
10
13
 
11
14
  export {
12
- ProductListSort,
13
- ProductListFilters,
14
15
  ProductListCount,
16
+ ProductListFilters,
17
+ ProductListFiltersContainer,
15
18
  ProductListItemsBase,
16
19
  ProductListPagination,
17
- ProductListFiltersContainer,
18
20
  ProductListParamsProvider,
21
+ ProductListSort,
19
22
  } from '@graphcommerce/magento-product'
20
23
 
21
24
  export * from '@graphcommerce/magento-product/components/ProductFiltersPro'
22
25
  export * from './utils/productListApplySearchDefaults'
26
+ export * from './components/ProductFiltersPro/ProductFiltersProSearchField'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-search",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "8.1.0-canary.9",
5
+ "version": "9.0.0-canary.101",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,16 +12,16 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/ecommerce-ui": "^8.1.0-canary.9",
16
- "@graphcommerce/eslint-config-pwa": "^8.1.0-canary.9",
17
- "@graphcommerce/graphql": "^8.1.0-canary.9",
18
- "@graphcommerce/image": "^8.1.0-canary.9",
19
- "@graphcommerce/magento-product": "^8.1.0-canary.9",
20
- "@graphcommerce/magento-store": "^8.1.0-canary.9",
21
- "@graphcommerce/next-ui": "^8.1.0-canary.9",
22
- "@graphcommerce/prettier-config-pwa": "^8.1.0-canary.9",
23
- "@graphcommerce/react-hook-form": "^8.1.0-canary.9",
24
- "@graphcommerce/typescript-config-pwa": "^8.1.0-canary.9",
15
+ "@graphcommerce/ecommerce-ui": "^9.0.0-canary.101",
16
+ "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.101",
17
+ "@graphcommerce/graphql": "^9.0.0-canary.101",
18
+ "@graphcommerce/image": "^9.0.0-canary.101",
19
+ "@graphcommerce/magento-product": "^9.0.0-canary.101",
20
+ "@graphcommerce/magento-store": "^9.0.0-canary.101",
21
+ "@graphcommerce/next-ui": "^9.0.0-canary.101",
22
+ "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.101",
23
+ "@graphcommerce/react-hook-form": "^9.0.0-canary.101",
24
+ "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.101",
25
25
  "@lingui/core": "^4.2.1",
26
26
  "@lingui/macro": "^4.2.1",
27
27
  "@lingui/react": "^4.2.1",
@@ -1,19 +1,50 @@
1
- import { cloneDeep } from '@graphcommerce/graphql'
2
- import { ProductListParams } from '@graphcommerce/magento-product'
3
- import { StoreConfigQuery } from '@graphcommerce/magento-store'
1
+ import { cloneDeep, useQuery } from '@graphcommerce/graphql'
2
+ import { ProductListParams, ProductListQueryVariables } from '@graphcommerce/magento-product'
3
+ import { StoreConfigDocument, StoreConfigQuery } from '@graphcommerce/magento-store'
4
4
 
5
+ export function useProductListApplySearchDefaults(
6
+ params: ProductListParams | undefined,
7
+ ): ProductListQueryVariables | undefined {
8
+ const storeConfig = useQuery(StoreConfigDocument)
9
+
10
+ if (!params) return params
11
+
12
+ const newParams = cloneDeep(params)
13
+
14
+ if (!newParams.pageSize) newParams.pageSize = storeConfig.data?.storeConfig?.grid_per_page ?? 12
15
+
16
+ if (Object.keys(params.sort).length === 0) {
17
+ newParams.sort = { relevance: 'DESC' }
18
+ }
19
+
20
+ return newParams
21
+ }
22
+
23
+ export function productListApplySearchDefaults(
24
+ params: ProductListParams,
25
+ conf: StoreConfigQuery,
26
+ ): ProductListQueryVariables
5
27
  export function productListApplySearchDefaults(
6
28
  params: ProductListParams | undefined,
7
29
  conf: StoreConfigQuery,
8
- ) {
30
+ ): ProductListQueryVariables | undefined {
9
31
  if (!params) return params
10
32
  const newParams = cloneDeep(params)
11
33
 
12
34
  if (!newParams.pageSize) newParams.pageSize = conf.storeConfig?.grid_per_page ?? 12
13
35
 
14
36
  if (Object.keys(newParams.sort).length === 0) {
15
- newParams.sort = { relevance: 'ASC' }
37
+ newParams.sort = { relevance: 'DESC' }
16
38
  }
17
39
 
18
40
  return newParams
19
41
  }
42
+
43
+ export function searchDefaultsToProductListFilters(
44
+ variables: ProductListQueryVariables | undefined,
45
+ ): ProductListQueryVariables {
46
+ return {
47
+ ...variables,
48
+ filters: {},
49
+ }
50
+ }