@graphcommerce/magento-search 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/CHANGELOG.md CHANGED
@@ -1,5 +1,197 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.0.0-canary.100
4
+
5
+ ## 9.0.0-canary.99
6
+
7
+ ## 9.0.0-canary.98
8
+
9
+ ## 9.0.0-canary.97
10
+
11
+ ## 9.0.0-canary.96
12
+
13
+ ## 9.0.0-canary.95
14
+
15
+ ## 9.0.0-canary.94
16
+
17
+ ## 9.0.0-canary.93
18
+
19
+ ## 9.0.0-canary.92
20
+
21
+ ## 9.0.0-canary.91
22
+
23
+ ## 9.0.0-canary.90
24
+
25
+ ## 9.0.0-canary.89
26
+
27
+ ## 9.0.0-canary.88
28
+
29
+ ## 9.0.0-canary.87
30
+
31
+ ## 9.0.0-canary.86
32
+
33
+ ## 9.0.0-canary.85
34
+
35
+ ## 9.0.0-canary.84
36
+
37
+ ## 9.0.0-canary.83
38
+
39
+ ## 9.0.0-canary.82
40
+
41
+ ## 9.0.0-canary.81
42
+
43
+ ## 9.0.0-canary.80
44
+
45
+ ## 9.0.0-canary.79
46
+
47
+ ## 9.0.0-canary.78
48
+
49
+ ## 9.0.0-canary.77
50
+
51
+ ## 9.0.0-canary.76
52
+
53
+ ## 9.0.0-canary.75
54
+
55
+ ## 9.0.0-canary.74
56
+
57
+ ## 9.0.0-canary.73
58
+
59
+ ## 9.0.0-canary.72
60
+
61
+ ## 9.0.0-canary.71
62
+
63
+ ## 9.0.0-canary.70
64
+
65
+ ## 9.0.0-canary.69
66
+
67
+ ## 9.0.0-canary.68
68
+
69
+ ## 9.0.0-canary.67
70
+
71
+ ## 9.0.0-canary.66
72
+
73
+ ## 9.0.0-canary.65
74
+
75
+ ## 9.0.0-canary.64
76
+
77
+ ## 9.0.0-canary.63
78
+
79
+ ## 9.0.0-canary.62
80
+
81
+ ## 9.0.0-canary.61
82
+
83
+ ## 9.0.0-canary.60
84
+
85
+ ## 9.0.0-canary.59
86
+
87
+ ### Patch Changes
88
+
89
+ - [#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))
90
+
91
+ ## 9.0.0-canary.58
92
+
93
+ ### Patch Changes
94
+
95
+ - [#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))
96
+
97
+ ## 9.0.0-canary.57
98
+
99
+ ## 9.0.0-canary.56
100
+
101
+ ## 9.0.0-canary.55
102
+
103
+ ## 9.0.0-canary.54
104
+
105
+ ## 8.1.0-canary.53
106
+
107
+ ## 8.1.0-canary.52
108
+
109
+ ## 8.1.0-canary.51
110
+
111
+ ## 8.1.0-canary.50
112
+
113
+ ## 8.1.0-canary.49
114
+
115
+ ## 8.1.0-canary.48
116
+
117
+ ## 8.1.0-canary.47
118
+
119
+ ## 8.1.0-canary.46
120
+
121
+ ## 8.1.0-canary.45
122
+
123
+ ## 8.1.0-canary.44
124
+
125
+ ## 8.1.0-canary.43
126
+
127
+ ## 8.1.0-canary.42
128
+
129
+ ## 8.1.0-canary.41
130
+
131
+ ## 8.1.0-canary.40
132
+
133
+ ## 8.1.0-canary.39
134
+
135
+ ## 8.1.0-canary.38
136
+
137
+ ## 8.1.0-canary.37
138
+
139
+ ## 8.1.0-canary.36
140
+
141
+ ## 8.1.0-canary.35
142
+
143
+ ## 8.1.0-canary.34
144
+
145
+ ## 8.1.0-canary.33
146
+
147
+ ## 8.1.0-canary.32
148
+
149
+ ## 8.1.0-canary.31
150
+
151
+ ## 8.1.0-canary.30
152
+
153
+ ## 8.1.0-canary.29
154
+
155
+ ## 8.1.0-canary.28
156
+
157
+ ## 8.1.0-canary.27
158
+
159
+ ## 8.1.0-canary.26
160
+
161
+ ## 8.1.0-canary.25
162
+
163
+ ## 8.1.0-canary.24
164
+
165
+ ## 8.1.0-canary.23
166
+
167
+ ## 8.1.0-canary.22
168
+
169
+ ## 8.1.0-canary.21
170
+
171
+ ## 8.1.0-canary.20
172
+
173
+ ## 8.1.0-canary.19
174
+
175
+ ## 8.1.0-canary.18
176
+
177
+ ## 8.1.0-canary.17
178
+
179
+ ## 8.1.0-canary.16
180
+
181
+ ## 8.1.0-canary.15
182
+
183
+ ## 8.1.0-canary.14
184
+
185
+ ## 8.1.0-canary.13
186
+
187
+ ## 8.1.0-canary.12
188
+
189
+ ## 8.1.0-canary.11
190
+
191
+ ## 8.1.0-canary.10
192
+
193
+ ## 8.1.0-canary.9
194
+
3
195
  ## 8.1.0-canary.8
4
196
 
5
197
  ## 8.1.0-canary.7
@@ -90,14 +282,11 @@
90
282
 
91
283
  ### Patch Changes
92
284
 
93
- - [#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.
94
- ([@paales](https://github.com/paales))
285
+ - [#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))
95
286
 
96
- - [`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
97
- ([@FrankHarland](https://github.com/FrankHarland))
287
+ - [`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))
98
288
 
99
- - [#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
100
- ([@paales](https://github.com/paales))
289
+ - [#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))
101
290
 
102
291
  ## 8.0.0-canary.100
103
292
 
@@ -149,8 +338,7 @@
149
338
 
150
339
  ### Patch Changes
151
340
 
152
- - [`e33660f`](https://github.com/graphcommerce-org/graphcommerce/commit/e33660f172466dcfa0ab7262cee612d9a3e47776) - a11y improvements (see https://github.com/graphcommerce-org/graphcommerce/issues/1995 for more info)
153
- ([@FrankHarland](https://github.com/FrankHarland))
341
+ - [`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))
154
342
 
155
343
  ## 8.0.0-canary.76
156
344
 
@@ -158,11 +346,9 @@
158
346
 
159
347
  ### Patch Changes
160
348
 
161
- - [#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
162
- ([@paales](https://github.com/paales))
349
+ - [#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))
163
350
 
164
- - [#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
165
- ([@paales](https://github.com/paales))
351
+ - [#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))
166
352
 
167
353
  ## 8.0.0-canary.74
168
354
 
@@ -1215,31 +1401,31 @@
1215
1401
  All occurences of `<Trans>` and `t` need to be replaced:
1216
1402
 
1217
1403
  ```tsx
1218
- import { Trans, t } from "@lingui/macro";
1404
+ import { Trans, t } from '@lingui/macro'
1219
1405
 
1220
1406
  function MyComponent() {
1221
- const foo = "bar";
1407
+ const foo = 'bar'
1222
1408
  return (
1223
1409
  <div aria-label={t`Account ${foo}`}>
1224
1410
  <Trans>My Translation {foo}</Trans>
1225
1411
  </div>
1226
- );
1412
+ )
1227
1413
  }
1228
1414
  ```
1229
1415
 
1230
1416
  Needs to be replaced with:
1231
1417
 
1232
1418
  ```tsx
1233
- import { Trans } from "@lingui/react";
1234
- import { i18n } from "@lingui/core";
1419
+ import { Trans } from '@lingui/react'
1420
+ import { i18n } from '@lingui/core'
1235
1421
 
1236
1422
  function MyComponent() {
1237
- const foo = "bar";
1423
+ const foo = 'bar'
1238
1424
  return (
1239
1425
  <div aria-label={i18n._(/* i18n */ `Account {foo}`, { foo })}>
1240
- <Trans key="My Translation {foo}" values={{ foo }}></Trans>
1426
+ <Trans key='My Translation {foo}' values={{ foo }}></Trans>
1241
1427
  </div>
1242
- );
1428
+ )
1243
1429
  }
1244
1430
  ```
1245
1431
 
@@ -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
+ }, [appliedAggregations, currentFilter, menu?.items])
152
+
153
+ if (!categoryTree) 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.8",
5
+ "version": "9.0.0-canary.100",
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.8",
16
- "@graphcommerce/eslint-config-pwa": "^8.1.0-canary.8",
17
- "@graphcommerce/graphql": "^8.1.0-canary.8",
18
- "@graphcommerce/image": "^8.1.0-canary.8",
19
- "@graphcommerce/magento-product": "^8.1.0-canary.8",
20
- "@graphcommerce/magento-store": "^8.1.0-canary.8",
21
- "@graphcommerce/next-ui": "^8.1.0-canary.8",
22
- "@graphcommerce/prettier-config-pwa": "^8.1.0-canary.8",
23
- "@graphcommerce/react-hook-form": "^8.1.0-canary.8",
24
- "@graphcommerce/typescript-config-pwa": "^8.1.0-canary.8",
15
+ "@graphcommerce/ecommerce-ui": "^9.0.0-canary.100",
16
+ "@graphcommerce/eslint-config-pwa": "^9.0.0-canary.100",
17
+ "@graphcommerce/graphql": "^9.0.0-canary.100",
18
+ "@graphcommerce/image": "^9.0.0-canary.100",
19
+ "@graphcommerce/magento-product": "^9.0.0-canary.100",
20
+ "@graphcommerce/magento-store": "^9.0.0-canary.100",
21
+ "@graphcommerce/next-ui": "^9.0.0-canary.100",
22
+ "@graphcommerce/prettier-config-pwa": "^9.0.0-canary.100",
23
+ "@graphcommerce/react-hook-form": "^9.0.0-canary.100",
24
+ "@graphcommerce/typescript-config-pwa": "^9.0.0-canary.100",
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
+ }