@graphcommerce/magento-store 9.1.0-canary.15 → 9.1.0-canary.17
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 +10 -0
- package/README.md +8 -0
- package/components/CurrencySymbol/CurrencySymbol.tsx +3 -2
- package/components/GlobalHead/GlobalHead.tsx +1 -1
- package/{Money.tsx → components/Money/Money.tsx} +1 -1
- package/{PageMeta.tsx → components/PageMeta/PageMeta.tsx} +1 -1
- package/components/StoreSwitcher/StoreSwitcherApply.tsx +16 -0
- package/components/StoreSwitcher/StoreSwitcherCurrencySelector.tsx +54 -0
- package/components/StoreSwitcher/StoreSwitcherGroupSelector.tsx +74 -0
- package/components/StoreSwitcher/StoreSwitcherList.graphql +21 -0
- package/components/{StoreSwitcherList → StoreSwitcher}/StoreSwitcherList.tsx +2 -6
- package/components/StoreSwitcher/StoreSwitcherStoreCurrencies.tsx +34 -0
- package/components/StoreSwitcher/StoreSwitcherStoreSelector.tsx +64 -0
- package/components/StoreSwitcher/index.ts +7 -0
- package/components/StoreSwitcher/useStoreSwitcher.tsx +203 -0
- package/components/StoreSwitcherButton/StoreSwitcherButton.tsx +12 -4
- package/docs/store-switcher-multiple-groups-one-store-multiple-currency.png +0 -0
- package/docs/store-switcher-multiple-groups-one-store-single-currency.png +0 -0
- package/docs/store-switcher-single-group-multiple-stores-multiple-currencies.png +0 -0
- package/docs/store-switcher-single-group-multiple-stores-single-currency.png +0 -0
- package/index.ts +8 -9
- package/mesh/resolvers.ts +14 -0
- package/package.json +10 -8
- package/plugins/magentoCurrencyCode.ts +35 -0
- package/plugins/magentoStoreGraphqlConfig.ts +8 -1
- package/plugins/meshMagentoStore.ts +23 -0
- package/queries/StoreConfig.graphql +6 -0
- package/queries/StoreConfigQueryFragment.graphql +6 -0
- package/schema/PrivateContext.graphqls +4 -0
- package/schema/StoreConfig-currency.graphqls +3 -0
- package/test/apolloClientStore.fixture.ts +1 -1
- package/utils/redirectOrNotFound.ts +2 -2
- package/StoreConfig.graphql +0 -5
- package/components/StoreSwitcherList/StoreSwitcherList.graphql +0 -10
- /package/{Money.graphql → components/Money/Money.graphql} +0 -0
- /package/{StoreConfigFragment.graphql → queries/StoreConfigFragment.graphql} +0 -0
- /package/{localeToStore.ts → utils/localeToStore.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 9.1.0-canary.17
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#2496](https://github.com/graphcommerce-org/graphcommerce/pull/2496) [`4b6a65d`](https://github.com/graphcommerce-org/graphcommerce/commit/4b6a65db089d92492e7fde28a8e32bc41f5b9103) - Added support for multiple display currencies in the frontend. Multiple currencies were already supported, but this introduces Display Currencies for viewing the cart in different currencies. ([@paales](https://github.com/paales))
|
|
8
|
+
|
|
9
|
+
- [#2496](https://github.com/graphcommerce-org/graphcommerce/pull/2496) [`4b6a65d`](https://github.com/graphcommerce-org/graphcommerce/commit/4b6a65db089d92492e7fde28a8e32bc41f5b9103) - Refactored the Store Selector to be more of a form and have multiple nested toggles to switch groups, then stores and then currencies. It automatically hides features that aren't used: If only a single group is used with multiple stores only the store selector is shown. If multiple groups are used with each a single store is used, only the group selector is shown. If only a single currency is used, there is no currency selector. If multiple currencies are used, the currency selector is shown. This makes the selector more user-friendly and less cluttered. ([@paales](https://github.com/paales))
|
|
10
|
+
|
|
11
|
+
## 9.1.0-canary.16
|
|
12
|
+
|
|
3
13
|
## 9.1.0-canary.15
|
|
4
14
|
|
|
5
15
|
## 9.0.4-canary.14
|
package/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @graphcommerce/magento-store
|
|
2
|
+
|
|
3
|
+
## Store Switcher
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
CurrencySymbol as CurrencySymbolBase,
|
|
4
4
|
type CurrencySymbolProps,
|
|
5
5
|
} from '@graphcommerce/next-ui'
|
|
6
|
-
import { StoreConfigDocument } from '../../StoreConfig.gql'
|
|
6
|
+
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
|
|
7
7
|
|
|
8
8
|
export function CurrencySymbol(props: CurrencySymbolProps) {
|
|
9
|
+
const { currency } = props
|
|
9
10
|
const baseCurrencyCode = useQuery(StoreConfigDocument).data?.storeConfig?.base_currency_code ?? ''
|
|
10
11
|
|
|
11
|
-
return <CurrencySymbolBase {...props} currency={baseCurrencyCode} />
|
|
12
|
+
return <CurrencySymbolBase {...props} currency={currency || baseCurrencyCode} />
|
|
12
13
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useQuery } from '@graphcommerce/graphql'
|
|
2
2
|
import type { GlobalHeadProps as GlobalHeadPropsBase } from '@graphcommerce/next-ui'
|
|
3
3
|
import { GlobalHead as GlobalHeadBase } from '@graphcommerce/next-ui'
|
|
4
|
-
import { StoreConfigDocument } from '../../StoreConfig.gql'
|
|
4
|
+
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
|
|
5
5
|
|
|
6
6
|
export type GlobalHeadProps = Omit<GlobalHeadPropsBase, 'name'>
|
|
7
7
|
|
|
@@ -2,8 +2,8 @@ import { useQuery } from '@graphcommerce/graphql'
|
|
|
2
2
|
import type { CurrencyFormatProps } from '@graphcommerce/next-ui'
|
|
3
3
|
import { CurrencyFormat } from '@graphcommerce/next-ui'
|
|
4
4
|
import type { SxProps, Theme } from '@mui/material'
|
|
5
|
+
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
|
|
5
6
|
import type { MoneyFragment } from './Money.gql'
|
|
6
|
-
import { StoreConfigDocument } from './StoreConfig.gql'
|
|
7
7
|
|
|
8
8
|
type OverridableProps = {
|
|
9
9
|
round?: boolean
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useQuery } from '@graphcommerce/graphql'
|
|
2
2
|
import type { PageMetaProps as NextPageMetaProps } from '@graphcommerce/next-ui'
|
|
3
3
|
import { PageMeta as NextPageMeta } from '@graphcommerce/next-ui'
|
|
4
|
-
import { StoreConfigDocument } from '
|
|
4
|
+
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
|
|
5
5
|
|
|
6
6
|
export type PageMetaProps = Omit<NextPageMetaProps, 'canonical'> & {
|
|
7
7
|
canonical?: string
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useFormState } from '@graphcommerce/ecommerce-ui'
|
|
2
|
+
import type { ButtonProps, LinkOrButtonProps } from '@graphcommerce/next-ui'
|
|
3
|
+
import { Button, LinkOrButton } from '@graphcommerce/next-ui'
|
|
4
|
+
import { useStoreSwitcherForm } from './useStoreSwitcher'
|
|
5
|
+
|
|
6
|
+
export function StoreSwitcherApplyButton(props: ButtonProps<'button'>) {
|
|
7
|
+
const { control } = useStoreSwitcherForm()
|
|
8
|
+
const formState = useFormState({ control })
|
|
9
|
+
return <Button type='submit' loading={formState.isSubmitting} {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StoreSwitcherLinkOrButton(props: LinkOrButtonProps) {
|
|
13
|
+
const { control } = useStoreSwitcherForm()
|
|
14
|
+
const formState = useFormState({ control })
|
|
15
|
+
return <LinkOrButton type='submit' loading={formState.isSubmitting} {...props} />
|
|
16
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ActionCardListFormProps } from '@graphcommerce/ecommerce-ui'
|
|
2
|
+
import { ActionCardListForm, useWatch } from '@graphcommerce/ecommerce-ui'
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '@graphcommerce/framer-utils'
|
|
4
|
+
import type { ActionCardProps } from '@graphcommerce/next-ui'
|
|
5
|
+
import { ActionCard, CurrencySymbol, nonNullable, sxx } from '@graphcommerce/next-ui'
|
|
6
|
+
import type { StoreSwitcherFormValues } from './useStoreSwitcher'
|
|
7
|
+
import { useSelectedStore, useStoreSwitcherForm } from './useStoreSwitcher'
|
|
8
|
+
|
|
9
|
+
export type StoreSwitcherCurrencySelectorProps = {
|
|
10
|
+
header?: React.ReactNode
|
|
11
|
+
} & Omit<
|
|
12
|
+
ActionCardListFormProps<ActionCardProps, StoreSwitcherFormValues>,
|
|
13
|
+
'name' | 'control' | 'items' | 'required' | 'render'
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export function StoreSwitcherCurrencySelector(props: StoreSwitcherCurrencySelectorProps) {
|
|
17
|
+
const { header, ...actionCardList } = props
|
|
18
|
+
const { control } = useStoreSwitcherForm()
|
|
19
|
+
const store = useSelectedStore()
|
|
20
|
+
|
|
21
|
+
const currencies = store?.currency?.available_currency_codes?.filter(nonNullable) ?? []
|
|
22
|
+
const defaultValue = store?.currency?.default_display_currency_code ?? currencies?.[0] ?? ''
|
|
23
|
+
|
|
24
|
+
// Make sure the default currency is the first in the list
|
|
25
|
+
currencies.sort((a, b) => {
|
|
26
|
+
if (a === defaultValue) return -1
|
|
27
|
+
if (b === defaultValue) return 1
|
|
28
|
+
return 0
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const hidden = currencies.length === 1
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{!hidden && header}
|
|
36
|
+
<ActionCardListForm<ActionCardProps, StoreSwitcherFormValues>
|
|
37
|
+
control={control}
|
|
38
|
+
defaultValue={defaultValue}
|
|
39
|
+
name='currency'
|
|
40
|
+
layout='stack'
|
|
41
|
+
size='responsive'
|
|
42
|
+
required
|
|
43
|
+
color='secondary'
|
|
44
|
+
render={ActionCard}
|
|
45
|
+
sx={sxx(hidden && { display: 'none !important' })}
|
|
46
|
+
items={currencies.map((currency) => ({
|
|
47
|
+
value: currency,
|
|
48
|
+
title: <CurrencySymbol currency={currency} variant='full' />,
|
|
49
|
+
}))}
|
|
50
|
+
{...actionCardList}
|
|
51
|
+
/>
|
|
52
|
+
</>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ActionCardListFormProps } from '@graphcommerce/ecommerce-ui'
|
|
2
|
+
import { ActionCardListForm } from '@graphcommerce/ecommerce-ui'
|
|
3
|
+
import type { ActionCardProps } from '@graphcommerce/next-ui'
|
|
4
|
+
import { ActionCard, FlagAvatar, ListFormat, sxx } from '@graphcommerce/next-ui'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import type { StoreSwitcherStoreCurrenciesProps } from './StoreSwitcherStoreCurrencies'
|
|
7
|
+
import { StoreSwitcherStoreCurrencies } from './StoreSwitcherStoreCurrencies'
|
|
8
|
+
import type { StoreSwitcherFormValues, StoreSwitcherGroup } from './useStoreSwitcher'
|
|
9
|
+
import { useStoreSwitcherForm } from './useStoreSwitcher'
|
|
10
|
+
|
|
11
|
+
export type StoreSwitcherGroupSelectorProps = {
|
|
12
|
+
header?: React.ReactNode
|
|
13
|
+
showStores?: number
|
|
14
|
+
} & Omit<StoreSwitcherStoreCurrenciesProps, 'store'> &
|
|
15
|
+
Omit<
|
|
16
|
+
ActionCardListFormProps<
|
|
17
|
+
ActionCardProps & { group?: StoreSwitcherGroup },
|
|
18
|
+
StoreSwitcherFormValues
|
|
19
|
+
>,
|
|
20
|
+
'name' | 'control' | 'items' | 'required' | 'render'
|
|
21
|
+
>
|
|
22
|
+
|
|
23
|
+
export function StoreSwitcherGroupSelector(props: StoreSwitcherGroupSelectorProps) {
|
|
24
|
+
const { header, showStores = 0, showCurrencies, ...actionCardList } = props
|
|
25
|
+
const { control, storeGroups } = useStoreSwitcherForm()
|
|
26
|
+
|
|
27
|
+
const hidden = storeGroups.length === 1 && storeGroups[0].stores.length > 1
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{!hidden && header}
|
|
32
|
+
<ActionCardListForm<ActionCardProps, StoreSwitcherFormValues>
|
|
33
|
+
control={control}
|
|
34
|
+
name='storeGroupCode'
|
|
35
|
+
layout='stack'
|
|
36
|
+
size='responsive'
|
|
37
|
+
required
|
|
38
|
+
color='secondary'
|
|
39
|
+
render={ActionCard}
|
|
40
|
+
sx={sxx(hidden && { display: 'none !important' })}
|
|
41
|
+
items={storeGroups.map((group) => ({
|
|
42
|
+
group,
|
|
43
|
+
image: group.country ? <FlagAvatar country={group.country} size='40px' /> : undefined,
|
|
44
|
+
title: <>{group.store_group_name}</>,
|
|
45
|
+
details: showStores && (
|
|
46
|
+
<>
|
|
47
|
+
{group.stores.length <= showStores && (
|
|
48
|
+
<ListFormat listStyle='short' type='unit'>
|
|
49
|
+
{group.stores.map((store) => (
|
|
50
|
+
<React.Fragment key={store.store_code}>
|
|
51
|
+
{store.store_name}
|
|
52
|
+
<StoreSwitcherStoreCurrencies
|
|
53
|
+
store={store}
|
|
54
|
+
showCurrencies={group.stores.length > 1 ? 0 : showCurrencies}
|
|
55
|
+
brackets
|
|
56
|
+
/>
|
|
57
|
+
</React.Fragment>
|
|
58
|
+
))}
|
|
59
|
+
</ListFormat>
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
62
|
+
),
|
|
63
|
+
disabled: group.disabled,
|
|
64
|
+
value: group.store_group_code,
|
|
65
|
+
slotProps: {
|
|
66
|
+
title: { sx: { typography: 'subtitle1' } },
|
|
67
|
+
details: { sx: { typography: 'body1', color: 'text.secondary' } },
|
|
68
|
+
},
|
|
69
|
+
}))}
|
|
70
|
+
{...actionCardList}
|
|
71
|
+
/>
|
|
72
|
+
</>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
query StoreSwitcherList {
|
|
2
|
+
availableStores {
|
|
3
|
+
store_name
|
|
4
|
+
store_code
|
|
5
|
+
locale
|
|
6
|
+
base_currency_code
|
|
7
|
+
store_group_name
|
|
8
|
+
store_group_code
|
|
9
|
+
currency {
|
|
10
|
+
available_currency_codes
|
|
11
|
+
base_currency_code
|
|
12
|
+
base_currency_symbol
|
|
13
|
+
default_display_currency_code
|
|
14
|
+
default_display_currency_symbol
|
|
15
|
+
exchange_rates {
|
|
16
|
+
currency_to
|
|
17
|
+
rate
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -3,7 +3,7 @@ import { extendableComponent, FlagAvatar, NextLink } from '@graphcommerce/next-u
|
|
|
3
3
|
import type { SxProps, Theme } from '@mui/material'
|
|
4
4
|
import { Collapse, List, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material'
|
|
5
5
|
import React from 'react'
|
|
6
|
-
import { localeToStore, storeToLocale } from '../../localeToStore'
|
|
6
|
+
import { localeToStore, storeToLocale } from '../../utils/localeToStore'
|
|
7
7
|
import type { StoreSwitcherListQuery } from './StoreSwitcherList.gql'
|
|
8
8
|
|
|
9
9
|
type Store = NonNullable<NonNullable<StoreSwitcherListQuery['availableStores']>[0]>
|
|
@@ -56,11 +56,7 @@ export function StoreSwitcherList(props: StoreSwitcherListProps) {
|
|
|
56
56
|
})}
|
|
57
57
|
>
|
|
58
58
|
<ListItemAvatar>
|
|
59
|
-
<FlagAvatar
|
|
60
|
-
country={code}
|
|
61
|
-
className={classes.avatar}
|
|
62
|
-
sx={{ width: 30, height: 30 }}
|
|
63
|
-
/>
|
|
59
|
+
<FlagAvatar country={code} className={classes.avatar} size='30px' />
|
|
64
60
|
</ListItemAvatar>
|
|
65
61
|
<ListItemText>
|
|
66
62
|
{group.name}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CurrencySymbolProps } from '@graphcommerce/next-ui'
|
|
2
|
+
import { CurrencySymbol, ListFormat, nonNullable } from '@graphcommerce/next-ui'
|
|
3
|
+
import type { StoreSwitcherStore } from './useStoreSwitcher'
|
|
4
|
+
|
|
5
|
+
export type StoreSwitcherStoreCurrenciesProps = {
|
|
6
|
+
showCurrencies?: number
|
|
7
|
+
store: StoreSwitcherStore
|
|
8
|
+
brackets?: boolean
|
|
9
|
+
variant?: CurrencySymbolProps['variant']
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StoreSwitcherStoreCurrencies(props: StoreSwitcherStoreCurrenciesProps) {
|
|
13
|
+
const { store, showCurrencies = 0, brackets = false, variant } = props
|
|
14
|
+
|
|
15
|
+
const currencies = (store.currency?.available_currency_codes ?? [])?.filter(nonNullable)
|
|
16
|
+
|
|
17
|
+
const show = currencies.length <= showCurrencies
|
|
18
|
+
const list = (
|
|
19
|
+
<ListFormat listStyle='short' type='unit'>
|
|
20
|
+
{currencies.map((currency) => (
|
|
21
|
+
<CurrencySymbol
|
|
22
|
+
key={currency}
|
|
23
|
+
compactDisplay='short'
|
|
24
|
+
variant={variant}
|
|
25
|
+
currency={currency}
|
|
26
|
+
/>
|
|
27
|
+
))}
|
|
28
|
+
</ListFormat>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (!show) return null
|
|
32
|
+
if (!brackets) return list
|
|
33
|
+
return <> ({list})</>
|
|
34
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ActionCardListFormProps } from '@graphcommerce/ecommerce-ui'
|
|
2
|
+
import { ActionCardListForm } from '@graphcommerce/ecommerce-ui'
|
|
3
|
+
import type { ActionCardProps } from '@graphcommerce/next-ui'
|
|
4
|
+
import { ActionCard, sxx } from '@graphcommerce/next-ui'
|
|
5
|
+
import type { StoreSwitcherStoreCurrenciesProps } from './StoreSwitcherStoreCurrencies'
|
|
6
|
+
import { StoreSwitcherStoreCurrencies } from './StoreSwitcherStoreCurrencies'
|
|
7
|
+
import type { StoreSwitcherFormValues, StoreSwitcherStore } from './useStoreSwitcher'
|
|
8
|
+
import { useSelectedStoreGroup, useStoreSwitcherForm } from './useStoreSwitcher'
|
|
9
|
+
|
|
10
|
+
export type StoreSwitcherSelectorProps = { header?: React.ReactNode } & Omit<
|
|
11
|
+
StoreSwitcherStoreCurrenciesProps,
|
|
12
|
+
'store'
|
|
13
|
+
> &
|
|
14
|
+
Omit<
|
|
15
|
+
ActionCardListFormProps<
|
|
16
|
+
ActionCardProps & { store?: StoreSwitcherStore },
|
|
17
|
+
StoreSwitcherFormValues
|
|
18
|
+
>,
|
|
19
|
+
'name' | 'control' | 'items' | 'required' | 'render'
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
export function StoreSwitcherStoreSelector(props: StoreSwitcherSelectorProps) {
|
|
23
|
+
const { header, showCurrencies, ...actionCardProps } = props
|
|
24
|
+
const { control } = useStoreSwitcherForm()
|
|
25
|
+
const selectedGroup = useSelectedStoreGroup()
|
|
26
|
+
|
|
27
|
+
const hidden = selectedGroup?.stores.length === 1
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{!hidden && header}
|
|
32
|
+
<ActionCardListForm<ActionCardProps, StoreSwitcherFormValues>
|
|
33
|
+
control={control}
|
|
34
|
+
defaultValue={selectedGroup?.stores?.[0].store_code ?? undefined}
|
|
35
|
+
shouldUnregister
|
|
36
|
+
name='storeCode'
|
|
37
|
+
layout='stack'
|
|
38
|
+
size='responsive'
|
|
39
|
+
required
|
|
40
|
+
render={ActionCard}
|
|
41
|
+
color='secondary'
|
|
42
|
+
sx={sxx(hidden && { display: 'none !important' })}
|
|
43
|
+
items={(selectedGroup?.stores ?? []).map((store) => ({
|
|
44
|
+
store,
|
|
45
|
+
title: store.store_name,
|
|
46
|
+
details: (
|
|
47
|
+
<StoreSwitcherStoreCurrencies
|
|
48
|
+
store={store}
|
|
49
|
+
showCurrencies={showCurrencies}
|
|
50
|
+
variant='full'
|
|
51
|
+
/>
|
|
52
|
+
),
|
|
53
|
+
value: store.store_code,
|
|
54
|
+
disabled: store.disabled,
|
|
55
|
+
slotProps: {
|
|
56
|
+
title: { sx: { typography: 'subtitle1' } },
|
|
57
|
+
details: { sx: { typography: 'body1', color: 'text.secondary' } },
|
|
58
|
+
},
|
|
59
|
+
}))}
|
|
60
|
+
{...actionCardProps}
|
|
61
|
+
/>
|
|
62
|
+
</>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './StoreSwitcherApply'
|
|
2
|
+
export * from './StoreSwitcherCurrencySelector'
|
|
3
|
+
export * from './StoreSwitcherGroupSelector'
|
|
4
|
+
export * from './StoreSwitcherList'
|
|
5
|
+
export * from './StoreSwitcherList.gql'
|
|
6
|
+
export * from './StoreSwitcherStoreSelector'
|
|
7
|
+
export * from './useStoreSwitcher'
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { SubmitHandler } from '@graphcommerce/ecommerce-ui'
|
|
2
|
+
import { useForm, useWatch, type UseFormReturn } from '@graphcommerce/ecommerce-ui'
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '@graphcommerce/framer-utils'
|
|
4
|
+
import {
|
|
5
|
+
cookie,
|
|
6
|
+
filterNonNullableKeys,
|
|
7
|
+
storefrontAll,
|
|
8
|
+
useStorefrontConfig,
|
|
9
|
+
type RequiredKeys,
|
|
10
|
+
} from '@graphcommerce/next-ui'
|
|
11
|
+
import { useRouter } from 'next/router'
|
|
12
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo } from 'react'
|
|
13
|
+
import { storeToLocale } from '../../utils/localeToStore'
|
|
14
|
+
import { type StoreSwitcherListQuery } from './StoreSwitcherList.gql'
|
|
15
|
+
|
|
16
|
+
export type StoreSwitcherStore = RequiredKeys<
|
|
17
|
+
NonNullable<NonNullable<StoreSwitcherListQuery['availableStores']>[0]>,
|
|
18
|
+
'store_code' | 'store_name' | 'store_group_name' | 'store_group_code'
|
|
19
|
+
> & {
|
|
20
|
+
country: string
|
|
21
|
+
disabled: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type StoreSwitcherGroup = Pick<
|
|
25
|
+
StoreSwitcherStore,
|
|
26
|
+
'store_group_code' | 'store_group_name'
|
|
27
|
+
> & {
|
|
28
|
+
stores: StoreSwitcherStore[]
|
|
29
|
+
country?: string
|
|
30
|
+
disabled: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type StoreSwitcherFormValues = {
|
|
34
|
+
storeGroupCode: string
|
|
35
|
+
storeCode: string
|
|
36
|
+
currency: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type StoreSwitcherFormContextType = {
|
|
40
|
+
stores: StoreSwitcherStore[]
|
|
41
|
+
storeGroups: StoreSwitcherGroup[]
|
|
42
|
+
} & UseFormReturn<StoreSwitcherFormValues>
|
|
43
|
+
|
|
44
|
+
const StoreSwitcherFormContext = createContext<StoreSwitcherFormContextType | null>(null)
|
|
45
|
+
|
|
46
|
+
export function useStoreSwitcherForm(
|
|
47
|
+
context?: StoreSwitcherFormContextType,
|
|
48
|
+
): StoreSwitcherFormContextType {
|
|
49
|
+
const ctx = useContext(StoreSwitcherFormContext) ?? context
|
|
50
|
+
if (!ctx) throw new Error('useStoreSwitcherForm must be used within a StoreSwitcherFormProvider')
|
|
51
|
+
return ctx
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useSelectedStoreGroup(
|
|
55
|
+
context?: StoreSwitcherFormContextType,
|
|
56
|
+
): StoreSwitcherGroup | null {
|
|
57
|
+
const { storeGroups, control } = useStoreSwitcherForm(context)
|
|
58
|
+
|
|
59
|
+
const selectedStoreGroupCode = useWatch({ control, name: 'storeGroupCode' })
|
|
60
|
+
const selectedStoreGroup = storeGroups.find(
|
|
61
|
+
(group) => group.store_group_code === selectedStoreGroupCode,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return selectedStoreGroup ?? null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useSelectedStore(
|
|
68
|
+
context?: StoreSwitcherFormContextType,
|
|
69
|
+
): StoreSwitcherStore | null {
|
|
70
|
+
const { control } = useStoreSwitcherForm(context)
|
|
71
|
+
|
|
72
|
+
const selectedStoreGroup = useSelectedStoreGroup(context)
|
|
73
|
+
|
|
74
|
+
const selectedStoreCode = useWatch({ control, name: 'storeCode' })
|
|
75
|
+
const selectedStore = selectedStoreGroup?.stores?.find(
|
|
76
|
+
(store) => store.store_code === selectedStoreCode,
|
|
77
|
+
)
|
|
78
|
+
return selectedStore ?? null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useSelectedCurrency(context?: StoreSwitcherFormContextType): string | null {
|
|
82
|
+
const { control } = useStoreSwitcherForm(context)
|
|
83
|
+
|
|
84
|
+
const selectedStore = useSelectedStore(context)
|
|
85
|
+
const selectedCurrencyCode = useWatch({ control, name: 'currency' })
|
|
86
|
+
|
|
87
|
+
return selectedStore?.currency?.available_currency_codes?.includes(selectedCurrencyCode)
|
|
88
|
+
? selectedCurrencyCode
|
|
89
|
+
: null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function useStoreSwitcher({
|
|
93
|
+
availableStores,
|
|
94
|
+
}: StoreSwitcherListQuery): StoreSwitcherFormContextType {
|
|
95
|
+
const stores: StoreSwitcherStore[] = useMemo(
|
|
96
|
+
() =>
|
|
97
|
+
filterNonNullableKeys(availableStores, [
|
|
98
|
+
'store_name',
|
|
99
|
+
'store_code',
|
|
100
|
+
'store_group_code',
|
|
101
|
+
'store_group_name',
|
|
102
|
+
'base_currency_code',
|
|
103
|
+
'locale',
|
|
104
|
+
])
|
|
105
|
+
.map((store) => ({
|
|
106
|
+
...store,
|
|
107
|
+
country: store.locale.split('_')[1]?.toLowerCase(),
|
|
108
|
+
disabled: !storefrontAll.find((l) => l.magentoStoreCode === store.store_code),
|
|
109
|
+
}))
|
|
110
|
+
.filter((store) => !store.disabled),
|
|
111
|
+
[availableStores],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const storeGroups = Object.values(
|
|
115
|
+
stores.reduce<{
|
|
116
|
+
[group: string]: StoreSwitcherGroup
|
|
117
|
+
}>((storesGrouped, store) => {
|
|
118
|
+
if (!storesGrouped[store.store_group_code]) {
|
|
119
|
+
const group: StoreSwitcherGroup = {
|
|
120
|
+
store_group_code: store.store_group_code,
|
|
121
|
+
store_group_name: store.store_group_name,
|
|
122
|
+
stores: [],
|
|
123
|
+
disabled: true,
|
|
124
|
+
country: store.country,
|
|
125
|
+
}
|
|
126
|
+
storesGrouped[store.store_group_code] = group
|
|
127
|
+
}
|
|
128
|
+
if (!store.disabled) storesGrouped[store.store_group_code].disabled = false
|
|
129
|
+
if (storesGrouped[store.store_group_code].country !== store.country) {
|
|
130
|
+
storesGrouped[store.store_group_code].country = undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
storesGrouped[store.store_group_code].stores.push(store)
|
|
134
|
+
return storesGrouped
|
|
135
|
+
}, {}),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const storeCode = useStorefrontConfig().magentoStoreCode
|
|
139
|
+
|
|
140
|
+
const form = useForm<StoreSwitcherFormValues>({
|
|
141
|
+
defaultValues: {
|
|
142
|
+
storeCode,
|
|
143
|
+
storeGroupCode:
|
|
144
|
+
stores.find((store) => store.store_code === storeCode)?.store_group_code ?? '',
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return { storeGroups, stores, ...form }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function StoreSwitcherFormProvider(
|
|
152
|
+
props: {
|
|
153
|
+
children?: React.ReactNode
|
|
154
|
+
onSubmit?: SubmitHandler<StoreSwitcherFormValues>
|
|
155
|
+
} & StoreSwitcherListQuery,
|
|
156
|
+
) {
|
|
157
|
+
const { children, onSubmit, availableStores } = props
|
|
158
|
+
const context = useStoreSwitcher({ availableStores })
|
|
159
|
+
const { setValue, storeGroups } = context
|
|
160
|
+
|
|
161
|
+
const selectedStoreGroup = useSelectedStoreGroup(context) ?? storeGroups[0]
|
|
162
|
+
const selectedStore = useSelectedStore(context)
|
|
163
|
+
const selectedCurrency = useSelectedCurrency(context)
|
|
164
|
+
|
|
165
|
+
useIsomorphicLayoutEffect(() => {
|
|
166
|
+
const currency = cookie('Magento-Content-Currency')
|
|
167
|
+
if (currency) setValue('currency', currency)
|
|
168
|
+
}, [setValue])
|
|
169
|
+
|
|
170
|
+
useIsomorphicLayoutEffect(() => {
|
|
171
|
+
const defaultStore = selectedStoreGroup.stores[0]
|
|
172
|
+
if (!selectedStore) {
|
|
173
|
+
setValue('storeCode', defaultStore.store_code)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const currencyCode =
|
|
177
|
+
selectedStore?.currency?.default_display_currency_code ??
|
|
178
|
+
defaultStore.currency?.default_display_currency_code
|
|
179
|
+
|
|
180
|
+
if (!selectedCurrency && currencyCode) {
|
|
181
|
+
setValue('currency', currencyCode)
|
|
182
|
+
}
|
|
183
|
+
}, [selectedCurrency, selectedStore, selectedStoreGroup.stores, setValue])
|
|
184
|
+
|
|
185
|
+
const submit = context.handleSubmit(async (data, event) => {
|
|
186
|
+
if (
|
|
187
|
+
data.currency ===
|
|
188
|
+
context.stores.find((store) => store.store_code === data.storeCode)?.base_currency_code
|
|
189
|
+
) {
|
|
190
|
+
cookie('Magento-Content-Currency', null)
|
|
191
|
+
} else {
|
|
192
|
+
cookie('Magento-Content-Currency', data.currency)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
onSubmit?.(data, event)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<StoreSwitcherFormContext.Provider value={context}>
|
|
200
|
+
<form onSubmit={submit}>{children}</form>
|
|
201
|
+
</StoreSwitcherFormContext.Provider>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useQuery } from '@graphcommerce/graphql'
|
|
2
|
-
import { extendableComponent, FlagAvatar } from '@graphcommerce/next-ui'
|
|
2
|
+
import { cookie, extendableComponent, FlagAvatar } from '@graphcommerce/next-ui'
|
|
3
3
|
import type { SxProps, Theme } from '@mui/material'
|
|
4
4
|
import { Button } from '@mui/material'
|
|
5
5
|
import { useRouter } from 'next/router'
|
|
6
|
-
import {
|
|
6
|
+
import { useMemo } from 'react'
|
|
7
|
+
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
|
|
7
8
|
|
|
8
9
|
export type StoreSwitcherButtonProps = { sx?: SxProps<Theme> }
|
|
9
10
|
|
|
@@ -13,10 +14,16 @@ const { classes } = extendableComponent(name, parts)
|
|
|
13
14
|
|
|
14
15
|
export function StoreSwitcherButton(props: StoreSwitcherButtonProps) {
|
|
15
16
|
const { sx } = props
|
|
17
|
+
|
|
16
18
|
const config = useQuery(StoreConfigDocument)
|
|
17
19
|
const country = config.data?.storeConfig?.locale?.split('_')?.[1]?.toLowerCase() ?? ''
|
|
18
20
|
const router = useRouter()
|
|
19
21
|
|
|
22
|
+
const currency = useMemo(
|
|
23
|
+
() => cookie('Magento-Content-Currency') ?? config.data?.storeConfig?.base_currency_code,
|
|
24
|
+
[config.data?.storeConfig?.base_currency_code],
|
|
25
|
+
)
|
|
26
|
+
|
|
20
27
|
return (
|
|
21
28
|
<Button
|
|
22
29
|
variant='text'
|
|
@@ -28,9 +35,10 @@ export function StoreSwitcherButton(props: StoreSwitcherButtonProps) {
|
|
|
28
35
|
<FlagAvatar
|
|
29
36
|
country={country}
|
|
30
37
|
className={classes.avatar}
|
|
31
|
-
|
|
38
|
+
size='20px'
|
|
39
|
+
sx={{ marginRight: '10px' }}
|
|
32
40
|
/>
|
|
33
|
-
{config.data?.storeConfig?.store_name}
|
|
41
|
+
{config.data?.storeConfig?.store_name} – {currency}
|
|
34
42
|
</Button>
|
|
35
43
|
)
|
|
36
44
|
}
|
package/index.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
export * from './components/CurrencySymbol/CurrencySymbol'
|
|
1
2
|
export * from './components/GlobalHead/GlobalHead'
|
|
3
|
+
export * from './components/Money/Money'
|
|
4
|
+
export * from './components/Money/Money.gql'
|
|
5
|
+
export * from './components/PageMeta/PageMeta'
|
|
6
|
+
export * from './components/StoreSwitcherButton/StoreSwitcherButton'
|
|
2
7
|
export * from './hooks/useFindCountry'
|
|
3
8
|
export * from './hooks/useFindRegion'
|
|
4
|
-
export * from './localeToStore'
|
|
5
|
-
export * from './Money'
|
|
6
|
-
export * from './Money.gql'
|
|
7
|
-
export * from './PageMeta'
|
|
8
9
|
export * from './queries/CountryRegions.gql'
|
|
9
|
-
export * from './StoreConfig.gql'
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './components/StoreSwitcherList/StoreSwitcherList'
|
|
12
|
-
export * from './components/StoreSwitcherList/StoreSwitcherList.gql'
|
|
10
|
+
export * from './queries/StoreConfig.gql'
|
|
11
|
+
export * from './utils/localeToStore'
|
|
13
12
|
export * from './utils/redirectOrNotFound'
|
|
14
|
-
export * from './components/
|
|
13
|
+
export * from './components/StoreSwitcher'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Resolvers } from '@graphcommerce/graphql-mesh'
|
|
2
|
+
|
|
3
|
+
export const resolvers: Resolvers = {
|
|
4
|
+
StoreConfig: {
|
|
5
|
+
currency: {
|
|
6
|
+
selectionSet: '{ store_code }',
|
|
7
|
+
resolve: (parent, args, ctx, info) => {
|
|
8
|
+
if (!parent.store_code) return null
|
|
9
|
+
const context = { ...ctx, headers: { store: parent.store_code } }
|
|
10
|
+
return context.m2.Query.currency({ context, info }) ?? null
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@graphcommerce/magento-store",
|
|
3
3
|
"homepage": "https://www.graphcommerce.org/",
|
|
4
4
|
"repository": "github:graphcommerce-org/graphcommerce",
|
|
5
|
-
"version": "9.1.0-canary.
|
|
5
|
+
"version": "9.1.0-canary.17",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
8
|
"eslintConfig": {
|
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@graphcommerce/
|
|
16
|
-
"@graphcommerce/
|
|
17
|
-
"@graphcommerce/
|
|
18
|
-
"@graphcommerce/
|
|
19
|
-
"@graphcommerce/
|
|
20
|
-
"@graphcommerce/
|
|
21
|
-
"@graphcommerce/
|
|
15
|
+
"@graphcommerce/ecommerce-ui": "^9.1.0-canary.17",
|
|
16
|
+
"@graphcommerce/eslint-config-pwa": "^9.1.0-canary.17",
|
|
17
|
+
"@graphcommerce/framer-utils": "^9.1.0-canary.17",
|
|
18
|
+
"@graphcommerce/graphql": "^9.1.0-canary.17",
|
|
19
|
+
"@graphcommerce/graphql-mesh": "^9.1.0-canary.17",
|
|
20
|
+
"@graphcommerce/image": "^9.1.0-canary.17",
|
|
21
|
+
"@graphcommerce/next-ui": "^9.1.0-canary.17",
|
|
22
|
+
"@graphcommerce/prettier-config-pwa": "^9.1.0-canary.17",
|
|
23
|
+
"@graphcommerce/typescript-config-pwa": "^9.1.0-canary.17",
|
|
22
24
|
"@lingui/core": "^4.2.1",
|
|
23
25
|
"@lingui/macro": "^4.2.1",
|
|
24
26
|
"@lingui/react": "^4.2.1",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type getPrivateQueryContext as getPrivateQueryContextType,
|
|
3
|
+
type usePrivateQueryContext as usePrivateQueryContextType,
|
|
4
|
+
} from '@graphcommerce/graphql'
|
|
5
|
+
import type { PrivateContext } from '@graphcommerce/graphql-mesh'
|
|
6
|
+
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
7
|
+
import { cookie } from '@graphcommerce/next-ui'
|
|
8
|
+
|
|
9
|
+
export const config: PluginConfig = {
|
|
10
|
+
type: 'function',
|
|
11
|
+
module: '@graphcommerce/graphql',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getPrivateQueryContext: FunctionPlugin<typeof getPrivateQueryContextType> = (
|
|
15
|
+
prev,
|
|
16
|
+
client,
|
|
17
|
+
...args
|
|
18
|
+
) => {
|
|
19
|
+
const currencyCode = cookie('Magento-Content-Currency')
|
|
20
|
+
|
|
21
|
+
const res = prev(client, ...args)
|
|
22
|
+
if (!currencyCode) return res
|
|
23
|
+
return { ...res, currencyCode } satisfies PrivateContext
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const usePrivateQueryContext: FunctionPlugin<typeof usePrivateQueryContextType> = (
|
|
27
|
+
prev,
|
|
28
|
+
...args
|
|
29
|
+
) => {
|
|
30
|
+
const currencyCode = cookie('Magento-Content-Currency')
|
|
31
|
+
|
|
32
|
+
const res = prev(...args)
|
|
33
|
+
if (!currencyCode) return res
|
|
34
|
+
return { ...res, currencyCode }
|
|
35
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setContext, type graphqlConfig as graphqlConfigType } from '@graphcommerce/graphql'
|
|
2
2
|
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
3
|
+
import { cookie } from '@graphcommerce/next-ui'
|
|
3
4
|
|
|
4
5
|
export const config: PluginConfig = {
|
|
5
6
|
type: 'function',
|
|
@@ -15,7 +16,13 @@ export const graphqlConfig: FunctionPlugin<typeof graphqlConfigType> = (prev, co
|
|
|
15
16
|
...results.links,
|
|
16
17
|
setContext((_, context) => {
|
|
17
18
|
if (!context.headers) context.headers = {}
|
|
18
|
-
context.headers.store
|
|
19
|
+
if (!context.headers.store) {
|
|
20
|
+
context.headers.store = conf.storefront.magentoStoreCode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const contentCurrency = cookie('Magento-Content-Currency')
|
|
24
|
+
if (contentCurrency && typeof context.headers['content-currency'] === 'undefined')
|
|
25
|
+
context.headers['content-currency'] = contentCurrency
|
|
19
26
|
|
|
20
27
|
if (conf.preview) {
|
|
21
28
|
// To disable caching from the backend, we provide a bogus cache ID.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { meshConfig as meshConfigBase } from '@graphcommerce/graphql-mesh/meshConfig'
|
|
2
|
+
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
3
|
+
|
|
4
|
+
export const config: PluginConfig = {
|
|
5
|
+
module: '@graphcommerce/graphql-mesh/meshConfig',
|
|
6
|
+
type: 'function',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const meshConfig: FunctionPlugin<typeof meshConfigBase> = (
|
|
10
|
+
prev,
|
|
11
|
+
baseConfig,
|
|
12
|
+
graphCommerceConfig,
|
|
13
|
+
) =>
|
|
14
|
+
prev(
|
|
15
|
+
{
|
|
16
|
+
...baseConfig,
|
|
17
|
+
additionalResolvers: [
|
|
18
|
+
...(baseConfig.additionalResolvers ?? []),
|
|
19
|
+
'@graphcommerce/magento-store/mesh/resolvers.ts',
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
graphCommerceConfig,
|
|
23
|
+
)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { NormalizedCacheObject } from '@graphcommerce/graphql'
|
|
5
5
|
import { ApolloClient, InMemoryCache } from '@graphcommerce/graphql'
|
|
6
6
|
import { test as base } from '@playwright/test'
|
|
7
|
-
import { localeToStore } from '../localeToStore'
|
|
7
|
+
import { localeToStore } from '../utils/localeToStore'
|
|
8
8
|
|
|
9
9
|
type ApolloClientStoreTest = {
|
|
10
10
|
apolloClient: ApolloClient<NormalizedCacheObject>
|
|
@@ -3,10 +3,10 @@ import type { ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@gr
|
|
|
3
3
|
import { flushMeasurePerf } from '@graphcommerce/graphql'
|
|
4
4
|
import { isTypename, nonNullable, storefrontConfig } from '@graphcommerce/next-ui'
|
|
5
5
|
import type { Redirect } from 'next'
|
|
6
|
-
import {
|
|
7
|
-
import type { StoreConfigQuery } from '../StoreConfig.gql'
|
|
6
|
+
import type { StoreConfigQuery } from '../queries/StoreConfig.gql'
|
|
8
7
|
import type { HandleRedirectQuery } from './HandleRedirect.gql'
|
|
9
8
|
import { HandleRedirectDocument } from './HandleRedirect.gql'
|
|
9
|
+
import { defaultLocale } from './localeToStore'
|
|
10
10
|
|
|
11
11
|
export type RedirectOr404Return = Promise<
|
|
12
12
|
| { redirect: Redirect; revalidate?: number | boolean }
|
package/StoreConfig.graphql
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|