@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 +206 -20
- package/components/NoSearchResults/NoSearchResults.tsx +3 -5
- package/components/ProductFiltersPro/ProductFiltersProCategorySectionSearch.tsx +170 -0
- package/components/ProductFiltersPro/ProductFiltersProSearchField.tsx +62 -0
- package/components/ProductFiltersPro/ProductFiltersProSearchHeader.tsx +36 -0
- package/components/ProductFiltersPro/ProductFiltersProSearchInput.tsx +123 -0
- package/components/ProductFiltersPro/useSearchPageAndParam.ts +12 -0
- package/components/SearchForm/SearchForm.tsx +3 -1
- package/hooks/useProductList.ts +66 -0
- package/index.ts +12 -8
- package/package.json +11 -11
- package/utils/productListApplySearchDefaults.ts +36 -5
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
|
1404
|
+
import { Trans, t } from '@lingui/macro'
|
1219
1405
|
|
1220
1406
|
function MyComponent() {
|
1221
|
-
const foo =
|
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
|
1234
|
-
import { i18n } from
|
1419
|
+
import { Trans } from '@lingui/react'
|
1420
|
+
import { i18n } from '@lingui/core'
|
1235
1421
|
|
1236
1422
|
function MyComponent() {
|
1237
|
-
const foo =
|
1423
|
+
const foo = 'bar'
|
1238
1424
|
return (
|
1239
1425
|
<div aria-label={i18n._(/* i18n */ `Account {foo}`, { foo })}>
|
1240
|
-
<Trans key=
|
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 = {
|
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 {
|
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
|
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) =>
|
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 './
|
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/
|
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": "
|
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": "^
|
16
|
-
"@graphcommerce/eslint-config-pwa": "^
|
17
|
-
"@graphcommerce/graphql": "^
|
18
|
-
"@graphcommerce/image": "^
|
19
|
-
"@graphcommerce/magento-product": "^
|
20
|
-
"@graphcommerce/magento-store": "^
|
21
|
-
"@graphcommerce/next-ui": "^
|
22
|
-
"@graphcommerce/prettier-config-pwa": "^
|
23
|
-
"@graphcommerce/react-hook-form": "^
|
24
|
-
"@graphcommerce/typescript-config-pwa": "^
|
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: '
|
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
|
+
}
|