@graphcommerce/magento-search 8.1.0-canary.9 → 9.0.0-canary.55
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 +92 -0
- package/components/NoSearchResults/NoSearchResults.tsx +3 -5
- package/components/ProductFiltersPro/ProductFiltersProCategorySectionSearch.tsx +170 -0
- package/components/ProductFiltersPro/ProductFiltersProSearchHeader.tsx +36 -0
- package/components/SearchForm/ProductFiltersProSearchField.tsx +66 -0
- package/components/SearchForm/ProductFiltersProSearchInput.tsx +113 -0
- package/components/SearchForm/SearchForm.tsx +3 -1
- package/components/SearchForm/useSearchPageAndParam.ts +12 -0
- package/hooks/useProductList.ts +64 -0
- package/index.ts +12 -8
- package/package.json +11 -11
- package/utils/productListApplySearchDefaults.ts +27 -5
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,97 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 9.0.0-canary.55
|
4
|
+
|
5
|
+
## 9.0.0-canary.54
|
6
|
+
|
7
|
+
## 8.1.0-canary.53
|
8
|
+
|
9
|
+
## 8.1.0-canary.52
|
10
|
+
|
11
|
+
## 8.1.0-canary.51
|
12
|
+
|
13
|
+
## 8.1.0-canary.50
|
14
|
+
|
15
|
+
## 8.1.0-canary.49
|
16
|
+
|
17
|
+
## 8.1.0-canary.48
|
18
|
+
|
19
|
+
## 8.1.0-canary.47
|
20
|
+
|
21
|
+
## 8.1.0-canary.46
|
22
|
+
|
23
|
+
## 8.1.0-canary.45
|
24
|
+
|
25
|
+
## 8.1.0-canary.44
|
26
|
+
|
27
|
+
## 8.1.0-canary.43
|
28
|
+
|
29
|
+
## 8.1.0-canary.42
|
30
|
+
|
31
|
+
## 8.1.0-canary.41
|
32
|
+
|
33
|
+
## 8.1.0-canary.40
|
34
|
+
|
35
|
+
## 8.1.0-canary.39
|
36
|
+
|
37
|
+
## 8.1.0-canary.38
|
38
|
+
|
39
|
+
## 8.1.0-canary.37
|
40
|
+
|
41
|
+
## 8.1.0-canary.36
|
42
|
+
|
43
|
+
## 8.1.0-canary.35
|
44
|
+
|
45
|
+
## 8.1.0-canary.34
|
46
|
+
|
47
|
+
## 8.1.0-canary.33
|
48
|
+
|
49
|
+
## 8.1.0-canary.32
|
50
|
+
|
51
|
+
## 8.1.0-canary.31
|
52
|
+
|
53
|
+
## 8.1.0-canary.30
|
54
|
+
|
55
|
+
## 8.1.0-canary.29
|
56
|
+
|
57
|
+
## 8.1.0-canary.28
|
58
|
+
|
59
|
+
## 8.1.0-canary.27
|
60
|
+
|
61
|
+
## 8.1.0-canary.26
|
62
|
+
|
63
|
+
## 8.1.0-canary.25
|
64
|
+
|
65
|
+
## 8.1.0-canary.24
|
66
|
+
|
67
|
+
## 8.1.0-canary.23
|
68
|
+
|
69
|
+
## 8.1.0-canary.22
|
70
|
+
|
71
|
+
## 8.1.0-canary.21
|
72
|
+
|
73
|
+
## 8.1.0-canary.20
|
74
|
+
|
75
|
+
## 8.1.0-canary.19
|
76
|
+
|
77
|
+
## 8.1.0-canary.18
|
78
|
+
|
79
|
+
## 8.1.0-canary.17
|
80
|
+
|
81
|
+
## 8.1.0-canary.16
|
82
|
+
|
83
|
+
## 8.1.0-canary.15
|
84
|
+
|
85
|
+
## 8.1.0-canary.14
|
86
|
+
|
87
|
+
## 8.1.0-canary.13
|
88
|
+
|
89
|
+
## 8.1.0-canary.12
|
90
|
+
|
91
|
+
## 8.1.0-canary.11
|
92
|
+
|
93
|
+
## 8.1.0-canary.10
|
94
|
+
|
3
95
|
## 8.1.0-canary.9
|
4
96
|
|
5
97
|
## 8.1.0-canary.8
|
@@ -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,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,66 @@
|
|
1
|
+
import { IconSvg, iconSearch, showPageLoadIndicator } from '@graphcommerce/next-ui'
|
2
|
+
import { Fab, FabProps } from '@mui/material'
|
3
|
+
import { useMemo, useRef, useState } from 'react'
|
4
|
+
import { ProductFiltersProSearchInputProps } from './ProductFiltersProSearchInput'
|
5
|
+
import { useSearchPageAndParam } from './useSearchPageAndParam'
|
6
|
+
import dynamic from 'next/dynamic'
|
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 (expanded !== searchPage) setExpanded(searchPage)
|
23
|
+
}, [expanded, searchPage])
|
24
|
+
|
25
|
+
const visible = expanded || searchPage
|
26
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
27
|
+
|
28
|
+
return (
|
29
|
+
<>
|
30
|
+
{visible && (
|
31
|
+
<ProductFiltersProSearchInputLazy
|
32
|
+
{...props}
|
33
|
+
formControl={formControl}
|
34
|
+
inputRef={inputRef}
|
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
|
+
{
|
55
|
+
display: {
|
56
|
+
xs: visible ? 'none' : 'inline-flex',
|
57
|
+
},
|
58
|
+
},
|
59
|
+
...(Array.isArray(fab?.sx) ? fab.sx : [fab?.sx]),
|
60
|
+
]}
|
61
|
+
>
|
62
|
+
<IconSvg src={iconSearch} size='large' />
|
63
|
+
</Fab>
|
64
|
+
</>
|
65
|
+
)
|
66
|
+
}
|
@@ -0,0 +1,113 @@
|
|
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
|
+
|
30
|
+
useEffect(() => {
|
31
|
+
// When the user is not focussed on the search field and the value gets updated, update the form.
|
32
|
+
if (internalRef.current && internalRef.current !== document.activeElement && searchParam)
|
33
|
+
internalRef.current.value = searchParam
|
34
|
+
}, [searchParam])
|
35
|
+
|
36
|
+
const result: P = {
|
37
|
+
...props,
|
38
|
+
inputRef: ref,
|
39
|
+
placeholder: t`Search all products...`,
|
40
|
+
name: 'search',
|
41
|
+
type: 'text',
|
42
|
+
onKeyDown: (e) => {
|
43
|
+
if (e.key === 'Enter') {
|
44
|
+
const context = globalFormContextRef.current
|
45
|
+
if (!context || !searchPage) {
|
46
|
+
return router.push(`/search/${e.currentTarget.value}`)
|
47
|
+
}
|
48
|
+
context.form.setValue('currentPage', 1)
|
49
|
+
context.form.setValue('search', e.currentTarget.value)
|
50
|
+
return context.submit()
|
51
|
+
}
|
52
|
+
props?.onKeyDown?.(e)
|
53
|
+
},
|
54
|
+
onChange: async (e) => {
|
55
|
+
const context = globalFormContextRef.current
|
56
|
+
|
57
|
+
// When we're not on the search page, we want to navigate as soon as possible.
|
58
|
+
// TODO: We only want to navigate once, and let the rest be handled by the search page.
|
59
|
+
if (!context || !searchPage) {
|
60
|
+
return router.push(`/search/${e.target.value}`)
|
61
|
+
}
|
62
|
+
|
63
|
+
context.form.setValue('currentPage', 1)
|
64
|
+
context.form.setValue('search', e.currentTarget.value)
|
65
|
+
await context.submit()
|
66
|
+
|
67
|
+
return props.onChange?.(e)
|
68
|
+
},
|
69
|
+
buttonProps: {
|
70
|
+
...buttonProps,
|
71
|
+
onClick: async (e) => {
|
72
|
+
const context = globalFormContextRef.current
|
73
|
+
|
74
|
+
if (context?.form.getValues('search')) {
|
75
|
+
context.form.setValue('currentPage', 1)
|
76
|
+
context.form.setValue('search', '')
|
77
|
+
if (internalRef.current) internalRef.current.value = ''
|
78
|
+
await context.submit()
|
79
|
+
} else if (searchPage) {
|
80
|
+
router.back()
|
81
|
+
if (internalRef.current) internalRef.current.value = ''
|
82
|
+
} else {
|
83
|
+
buttonProps.onClick?.(e)
|
84
|
+
}
|
85
|
+
},
|
86
|
+
},
|
87
|
+
}
|
88
|
+
return result
|
89
|
+
}
|
90
|
+
|
91
|
+
export type ProductFiltersProSearchInputProps = OutlinedInputProps & {
|
92
|
+
formControl?: FormControlProps
|
93
|
+
buttonProps?: IconButtonProps
|
94
|
+
}
|
95
|
+
|
96
|
+
export function ProductFiltersProSearchOutlinedInput(props: ProductFiltersProSearchInputProps) {
|
97
|
+
const { buttonProps, formControl, size, ...rest } = useProductFiltersProSearchInput(props)
|
98
|
+
|
99
|
+
return (
|
100
|
+
<FormControl variant='outlined' size={size} {...formControl}>
|
101
|
+
<OutlinedInput
|
102
|
+
color='primary'
|
103
|
+
size={size}
|
104
|
+
endAdornment={
|
105
|
+
<IconButton color='inherit' size='small' {...buttonProps}>
|
106
|
+
<IconSvg src={iconClose} size='large' />
|
107
|
+
</IconButton>
|
108
|
+
}
|
109
|
+
{...rest}
|
110
|
+
/>
|
111
|
+
</FormControl>
|
112
|
+
)
|
113
|
+
}
|
@@ -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,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
|
+
}
|
@@ -0,0 +1,64 @@
|
|
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
|
+
useProductListApplySearchDefaults,
|
18
|
+
} from '../utils/productListApplySearchDefaults'
|
19
|
+
|
20
|
+
/**
|
21
|
+
* - Handles shallow routing requests
|
22
|
+
* - Handles customer specific product list queries
|
23
|
+
* - Creates a prefetch function to preload the product list
|
24
|
+
*/
|
25
|
+
export function useProductList<
|
26
|
+
T extends ProductListQuery &
|
27
|
+
ProductFiltersQuery & {
|
28
|
+
params?: ProductListParams
|
29
|
+
},
|
30
|
+
>(props: T) {
|
31
|
+
const { params, shallow } = useRouterFilterParams(props)
|
32
|
+
const variables = useProductListApplySearchDefaults(params)
|
33
|
+
const result = useInContextQuery(ProductListDocument, { variables, skip: !shallow }, props)
|
34
|
+
|
35
|
+
const filters = useInContextQuery(
|
36
|
+
ProductFiltersDocument,
|
37
|
+
{ variables: { search: params?.search }, 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
|
+
await prefetchProductList(
|
48
|
+
productListApplySearchDefaults(toProductListParams(formValues), storeConfig),
|
49
|
+
next,
|
50
|
+
result.client,
|
51
|
+
true,
|
52
|
+
)
|
53
|
+
},
|
54
|
+
)
|
55
|
+
|
56
|
+
return {
|
57
|
+
...props,
|
58
|
+
filters: filters.data.filters,
|
59
|
+
...result.data,
|
60
|
+
params,
|
61
|
+
mask: result.mask,
|
62
|
+
handleSubmit,
|
63
|
+
}
|
64
|
+
}
|
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/SearchForm/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.55",
|
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.55",
|
16
|
+
"@graphcommerce/eslint-config-pwa": "^9.0.0-canary.55",
|
17
|
+
"@graphcommerce/graphql": "^9.0.0-canary.55",
|
18
|
+
"@graphcommerce/image": "^9.0.0-canary.55",
|
19
|
+
"@graphcommerce/magento-product": "^9.0.0-canary.55",
|
20
|
+
"@graphcommerce/magento-store": "^9.0.0-canary.55",
|
21
|
+
"@graphcommerce/next-ui": "^9.0.0-canary.55",
|
22
|
+
"@graphcommerce/prettier-config-pwa": "^9.0.0-canary.55",
|
23
|
+
"@graphcommerce/react-hook-form": "^9.0.0-canary.55",
|
24
|
+
"@graphcommerce/typescript-config-pwa": "^9.0.0-canary.55",
|
25
25
|
"@lingui/core": "^4.2.1",
|
26
26
|
"@lingui/macro": "^4.2.1",
|
27
27
|
"@lingui/react": "^4.2.1",
|
@@ -1,18 +1,40 @@
|
|
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
|