@spree/dashboard-core 1.0.0-rc.3 → 1.0.0-rc.5
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/package.json +14 -14
- package/src/api-client.ts +12 -7
- package/src/components/address-form-dialog.tsx +2 -2
- package/src/components/country-combobox.tsx +1 -2
- package/src/components/currency-select.tsx +8 -10
- package/src/components/file-upload-field.tsx +1 -0
- package/src/components/import-wizard-dialog.tsx +8 -11
- package/src/components/locale-select.tsx +22 -23
- package/src/components/nav-main.tsx +7 -2
- package/src/components/preferences-form.tsx +18 -10
- package/src/components/resource-combobox.tsx +5 -0
- package/src/components/resource-picker-sheet.tsx +18 -12
- package/src/components/settings-sidebar.tsx +8 -1
- package/src/components/slot.tsx +21 -1
- package/src/components/table-toolbar.tsx +111 -45
- package/src/hooks/use-display-name.ts +1 -18
- package/src/hooks/use-file-upload.ts +2 -1
- package/src/hooks/use-import.ts +3 -2
- package/src/index.ts +13 -3
- package/src/lib/currencies.ts +2 -18
- package/src/lib/currency-parts.ts +2 -21
- package/src/lib/filters-to-ransack.ts +4 -1
- package/src/lib/form-fields-registry.ts +23 -1
- package/src/lib/form-mappers.ts +1 -1
- package/src/lib/formatters.ts +6 -10
- package/src/lib/fulfillment-items.ts +11 -0
- package/src/lib/i18n.ts +1 -12
- package/src/lib/iso-codes.ts +9 -24
- package/src/lib/money.ts +9 -99
- package/src/lib/normalize-money.ts +2 -85
- package/src/lib/permissions.ts +2 -0
- package/src/lib/return-refunds.ts +1 -2
- package/src/lib/route-registry.ts +29 -0
- package/src/lib/settings-nav-registry.test.ts +34 -0
- package/src/lib/settings-nav-registry.ts +27 -5
- package/src/lib/store-config.test.ts +93 -0
- package/src/lib/store-config.ts +76 -0
- package/src/lib/table-registry.ts +16 -5
- package/src/locales/ar.json +7 -1
- package/src/locales/de.json +7 -1
- package/src/locales/en.json +7 -1
- package/src/locales/es.json +7 -1
- package/src/locales/fr.json +7 -1
- package/src/locales/pl.json +7 -1
- package/src/locales/zh-CN.json +7 -1
- package/src/pages/package-types.tsx +50 -11
- package/src/pages/settings-index.tsx +4 -1
- package/src/pages/stock-locations.tsx +31 -6
- package/src/products/bulk-variants-dialog.tsx +28 -35
- package/src/products/inventory-section.tsx +2 -2
- package/src/products/media-edit-sheet.tsx +67 -8
- package/src/products/normalize-quantity.ts +2 -3
- package/src/products/product-bulk-price-editor.tsx +1 -3
- package/src/products/product-form-cards.tsx +67 -37
- package/src/products/product-form-mapping.ts +44 -5
- package/src/products/product-schema.test.ts +8 -0
- package/src/products/product-schema.ts +14 -7
- package/src/products/product-types.ts +1 -1
- package/src/products/sheet-save-handlers.ts +37 -0
- package/src/products/variant-edit-sheet.tsx +21 -11
- package/src/products/variants-options-builder.tsx +30 -27
- package/src/products/variants-section.tsx +35 -26
- package/src/providers/store-provider.tsx +31 -7
- package/src/schemas/package-type.ts +14 -12
- package/src/schemas/stock-location.ts +2 -3
- package/src/tables/stock-locations.tsx +5 -8
- package/src/lib/money.test.ts +0 -63
- package/src/lib/normalize-money.test.ts +0 -67
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/dashboard-core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.5",
|
|
5
5
|
"description": "Spree Dashboard framework — registries (table, nav, slot, settings-nav), providers (auth, permission, store), admin SDK client, generic infra hooks, and the defineDashboardPlugin extension API. Developer Preview.",
|
|
6
6
|
"author": "Vendo Connect Inc.",
|
|
7
7
|
"homepage": "https://spreecommerce.org",
|
|
@@ -49,13 +49,10 @@
|
|
|
49
49
|
"LICENSE"
|
|
50
50
|
],
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@
|
|
53
|
-
"@spree/admin-sdk": ">=0.7.0 <1.0.0-0 || >=1.0.0-0 <2.0.0-0",
|
|
52
|
+
"@spree/admin-sdk": ">=1.0.0-rc.4 <2.0.0-0",
|
|
54
53
|
"@spree/dashboard-ui": ">=0.10.0 <1.0.0-0 || >=1.0.0-0 <2.0.0-0",
|
|
55
|
-
"@tanstack/react-hotkeys": "^0.10.0",
|
|
56
54
|
"@tanstack/react-query": "^5.100.9",
|
|
57
55
|
"@tanstack/react-router": "^1.169.2",
|
|
58
|
-
"@tanstack/react-table": "^8.21.3",
|
|
59
56
|
"i18next": "^26.2.0",
|
|
60
57
|
"react": "^19.2.6",
|
|
61
58
|
"react-dom": "^19.2.6",
|
|
@@ -66,21 +63,24 @@
|
|
|
66
63
|
"@dnd-kit/core": "^6.3.1",
|
|
67
64
|
"@dnd-kit/sortable": "^10.0.0",
|
|
68
65
|
"@dnd-kit/utilities": "^3.2.2",
|
|
66
|
+
"@hookform/resolvers": "^5.2.2",
|
|
69
67
|
"@tailwindcss/vite": "^4.2.4",
|
|
70
|
-
"
|
|
68
|
+
"@tanstack/react-hotkeys": "^0.14.3",
|
|
69
|
+
"date-fns": "^4.4.0",
|
|
71
70
|
"date-fns-tz": "^3.2.0",
|
|
72
|
-
"lucide-react": "^1.
|
|
73
|
-
"zod": "^4.
|
|
71
|
+
"lucide-react": "^1.58.0",
|
|
72
|
+
"zod": "^4.6.7",
|
|
73
|
+
"@spree/core": "^0.2.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/react": "^19.2.14",
|
|
77
77
|
"@types/react-dom": "^19.2.3",
|
|
78
|
-
"tsup": "^
|
|
79
|
-
"typescript": "~
|
|
80
|
-
"vite": "^8.
|
|
81
|
-
"vitest": "^
|
|
82
|
-
"@spree/
|
|
83
|
-
"@spree/
|
|
78
|
+
"tsup": "^9.0.0",
|
|
79
|
+
"typescript": "~7.0.2",
|
|
80
|
+
"vite": "^8.3.4",
|
|
81
|
+
"vitest": "^5.0.5",
|
|
82
|
+
"@spree/admin-sdk": "^1.0.0-rc.5",
|
|
83
|
+
"@spree/dashboard-ui": "^1.0.0-rc.5"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
86
|
"lint": "biome check src",
|
package/src/api-client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PackageTypeKind, PickupStockPolicy } from '@spree/core'
|
|
1
2
|
/**
|
|
2
3
|
* The client registry.
|
|
3
4
|
*
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
* promise the seller panel methods its API does not have.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
import type {
|
|
18
|
+
import type { PermissionRule, UploadFileOptions } from '@spree/admin-sdk'
|
|
19
|
+
import type { CountryCode, DimensionUnit, WeightUnit } from '@spree/core'
|
|
18
20
|
|
|
19
21
|
/** What a panel may change on the signed-in person's own account. */
|
|
20
22
|
export interface PanelAccountParams {
|
|
@@ -354,10 +356,10 @@ export interface PanelPackageTypeParams {
|
|
|
354
356
|
length?: number | null
|
|
355
357
|
width?: number | null
|
|
356
358
|
height?: number | null
|
|
357
|
-
dimensions_unit?:
|
|
359
|
+
dimensions_unit?: DimensionUnit | null
|
|
358
360
|
weight?: number | null
|
|
359
361
|
max_weight?: number | null
|
|
360
|
-
weight_unit?:
|
|
362
|
+
weight_unit?: WeightUnit | null
|
|
361
363
|
default?: boolean
|
|
362
364
|
/**
|
|
363
365
|
* Not nullable: both SDKs take a merge, so clearing a key is sending the
|
|
@@ -386,9 +388,12 @@ export interface PanelPackageTypeWrites extends PanelPackageTypeReads {
|
|
|
386
388
|
delete?(id: string): Promise<void>
|
|
387
389
|
}
|
|
388
390
|
|
|
389
|
-
|
|
390
|
-
export type
|
|
391
|
-
|
|
391
|
+
/** @deprecated Use `PackageTypeKind` from `@spree/core`. */
|
|
392
|
+
export type PanelPackageTypeKind = PackageTypeKind
|
|
393
|
+
/** @deprecated Use `DimensionUnit` from `@spree/core`. */
|
|
394
|
+
export type PanelPackageDimensionUnit = DimensionUnit
|
|
395
|
+
/** @deprecated Use `WeightUnit` from `@spree/core`. */
|
|
396
|
+
export type PanelPackageWeightUnit = WeightUnit
|
|
392
397
|
|
|
393
398
|
/**
|
|
394
399
|
* A new row must say what it is and what to call it — packaging without
|
|
@@ -598,7 +603,7 @@ export type PanelStockLocationParams = Partial<
|
|
|
598
603
|
* because plugins register their own, but the pickup policy is a fixed pair
|
|
599
604
|
* the server validates, and both SDKs type it that way.
|
|
600
605
|
*/
|
|
601
|
-
pickup_stock_policy?:
|
|
606
|
+
pickup_stock_policy?: PickupStockPolicy
|
|
602
607
|
}
|
|
603
608
|
|
|
604
609
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
-
import type
|
|
2
|
+
import { type CountryCode, isCountryCode } from '@spree/core'
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
5
5
|
Checkbox,
|
|
@@ -22,7 +22,7 @@ import { Controller, type DefaultValues, useForm } from 'react-hook-form'
|
|
|
22
22
|
import { useTranslation } from 'react-i18next'
|
|
23
23
|
import { z } from 'zod/v4'
|
|
24
24
|
import { mapSpreeErrorsToForm } from '../lib/form-errors'
|
|
25
|
-
import { countryCodeSchema
|
|
25
|
+
import { countryCodeSchema } from '../lib/iso-codes'
|
|
26
26
|
import { useOptionalStore } from '../providers/store-provider'
|
|
27
27
|
import { CountryCombobox } from './country-combobox'
|
|
28
28
|
import { StateCombobox, useCountryStates } from './country-state-fields'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CountryCode, isCountryCode } from '@spree/core'
|
|
2
2
|
import {
|
|
3
3
|
Combobox,
|
|
4
4
|
ComboboxButtonTrigger,
|
|
@@ -19,7 +19,6 @@ import { useCallback, useMemo, useState } from 'react'
|
|
|
19
19
|
import { useTranslation } from 'react-i18next'
|
|
20
20
|
import { useCountries } from '../hooks/use-countries'
|
|
21
21
|
import { useDisplayName } from '../hooks/use-display-name'
|
|
22
|
-
import { isCountryCode } from '../lib/iso-codes'
|
|
23
22
|
|
|
24
23
|
export type CountryOption = { iso: CountryCode; iso3: string }
|
|
25
24
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CurrencyCode, isCurrencyCode } from '@spree/core'
|
|
2
2
|
import {
|
|
3
3
|
Combobox,
|
|
4
4
|
ComboboxButtonTrigger,
|
|
@@ -12,10 +12,9 @@ import {
|
|
|
12
12
|
import { useMemo, useState } from 'react'
|
|
13
13
|
import { useTranslation } from 'react-i18next'
|
|
14
14
|
import { useDisplayName } from '../hooks/use-display-name'
|
|
15
|
-
import { isCurrencyCode } from '../lib/iso-codes'
|
|
16
15
|
import { useOptionalStore } from '../providers/store-provider'
|
|
17
16
|
|
|
18
|
-
export { ALL_CURRENCY_CODES } from '
|
|
17
|
+
export { ACTIVE_CURRENCY_CODES as ALL_CURRENCY_CODES } from '@spree/core'
|
|
19
18
|
|
|
20
19
|
interface CurrencySelectProps {
|
|
21
20
|
/** Classes for the trigger — used to match a neighbouring control's height. */
|
|
@@ -33,8 +32,8 @@ interface CurrencySelectProps {
|
|
|
33
32
|
/** Fires on every selection change. */
|
|
34
33
|
onChange?: (currency: CurrencyCode) => void
|
|
35
34
|
/**
|
|
36
|
-
* Currency codes to pick from. Defaults to the current
|
|
37
|
-
*
|
|
35
|
+
* Currency codes to pick from. Defaults to the currencies of the current
|
|
36
|
+
* store's markets. Pass `ALL_CURRENCY_CODES` (or any custom list) for
|
|
38
37
|
* contexts where the merchant chooses a currency the store doesn't yet
|
|
39
38
|
* support — e.g. a market's currency.
|
|
40
39
|
*/
|
|
@@ -55,8 +54,8 @@ interface CurrencySelectProps {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
/**
|
|
58
|
-
* Picker for a currency code. Defaults to the
|
|
59
|
-
*
|
|
57
|
+
* Picker for a currency code. Defaults to the currencies of the
|
|
58
|
+
* current store's markets (the default market's first), so callers
|
|
60
59
|
* choosing among already-configured currencies don't have to wire anything up.
|
|
61
60
|
* Pass `options` (e.g. `ALL_CURRENCY_CODES`) when the merchant may pick any
|
|
62
61
|
* currency, such as a market's currency. Each option reads `CODE — Full Name`
|
|
@@ -103,9 +102,8 @@ export function CurrencySelect({
|
|
|
103
102
|
|
|
104
103
|
// Union of the option list and the current value so editing a record whose
|
|
105
104
|
// currency isn't in the list (a store-supported list that later narrowed)
|
|
106
|
-
// never silently drops the selection. An empty `options`
|
|
107
|
-
//
|
|
108
|
-
// falls back to the store's currencies rather than an empty picker.
|
|
105
|
+
// never silently drops the selection. An empty `options` falls back to the
|
|
106
|
+
// store's currencies rather than an empty picker.
|
|
109
107
|
const items = useMemo(() => {
|
|
110
108
|
const base = options?.length ? options : currencies
|
|
111
109
|
return isCurrencyCode(value) && !base.includes(value) ? [value, ...base] : base
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Alert,
|
|
3
|
+
AlertDescription,
|
|
2
4
|
Badge,
|
|
3
5
|
Button,
|
|
4
6
|
Card,
|
|
@@ -33,6 +35,7 @@ import {
|
|
|
33
35
|
CheckCircle2Icon,
|
|
34
36
|
ChevronLeftIcon,
|
|
35
37
|
ChevronRightIcon,
|
|
38
|
+
CircleAlertIcon,
|
|
36
39
|
DownloadIcon,
|
|
37
40
|
RotateCcwIcon,
|
|
38
41
|
XIcon,
|
|
@@ -107,13 +110,7 @@ export function ImportWizardDialog({ importId, onClose, onViewRecords }: ImportW
|
|
|
107
110
|
}}
|
|
108
111
|
modal
|
|
109
112
|
>
|
|
110
|
-
<DialogContent
|
|
111
|
-
// Edge-to-edge minus a 3-unit gutter — see BulkPriceEditorDialog for
|
|
112
|
-
// why every inset/translate/max is overridden.
|
|
113
|
-
className="!inset-3 !w-auto !max-w-none !translate-x-0 !translate-y-0 flex flex-col p-0"
|
|
114
|
-
style={{ maxHeight: 'none' }}
|
|
115
|
-
showCloseButton={false}
|
|
116
|
-
>
|
|
113
|
+
<DialogContent fullScreen showCloseButton={false}>
|
|
117
114
|
{importId && (
|
|
118
115
|
<ImportWizard
|
|
119
116
|
importId={importId}
|
|
@@ -472,10 +469,10 @@ function MappingStep({
|
|
|
472
469
|
</div>
|
|
473
470
|
|
|
474
471
|
{error && (
|
|
475
|
-
<
|
|
476
|
-
<
|
|
477
|
-
<
|
|
478
|
-
</
|
|
472
|
+
<Alert variant="destructive">
|
|
473
|
+
<CircleAlertIcon />
|
|
474
|
+
<AlertDescription>{error.message}</AlertDescription>
|
|
475
|
+
</Alert>
|
|
479
476
|
)}
|
|
480
477
|
</CardContent>
|
|
481
478
|
</Card>
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
ComboboxValue,
|
|
14
14
|
useComboboxAnchor,
|
|
15
15
|
} from '@spree/dashboard-ui'
|
|
16
|
-
import { useMemo, useState } from 'react'
|
|
16
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
17
17
|
import { useTranslation } from 'react-i18next'
|
|
18
18
|
import { useDisplayName } from '../hooks/use-display-name'
|
|
19
19
|
import { useStore } from '../providers/store-provider'
|
|
20
20
|
|
|
21
21
|
interface BaseProps {
|
|
22
|
-
/** Locale codes to pick from. Defaults to the current store's
|
|
22
|
+
/** Locale codes to pick from. Defaults to the languages of the current store's markets. */
|
|
23
23
|
options?: string[]
|
|
24
24
|
/** Locale to filter out (e.g. the default locale when picking additional supported ones). */
|
|
25
25
|
excludeCode?: string
|
|
@@ -63,16 +63,6 @@ interface MultiProps extends BaseProps {
|
|
|
63
63
|
|
|
64
64
|
type LocaleSelectProps = SingleProps | MultiProps
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* Resolves a locale code (`en`, `pt-BR`) to its display name (`English`,
|
|
68
|
-
* `Portuguese (Brazil)`) in the admin UI language, falling back to the code
|
|
69
|
-
* itself when the runtime lacks coverage.
|
|
70
|
-
*/
|
|
71
|
-
function useLocaleDisplayName() {
|
|
72
|
-
const displayName = useDisplayName('language')
|
|
73
|
-
return (code: string) => displayName(code) ?? code
|
|
74
|
-
}
|
|
75
|
-
|
|
76
66
|
/**
|
|
77
67
|
* Mirrors `CurrencySelect`'s row format: `CODE — Localized Name`. Codes the
|
|
78
68
|
* runtime cannot name (`Intl.DisplayNames` returns the code back, as it does
|
|
@@ -96,6 +86,16 @@ function localeFilter(labelFor: (locale: string) => string) {
|
|
|
96
86
|
}
|
|
97
87
|
}
|
|
98
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Returns a function that labels a locale code the way the dashboard shows
|
|
91
|
+
* locales everywhere: `EN — English`, named in the admin UI language. For
|
|
92
|
+
* plain-text contexts (option lists, search); render `LocaleLabel` in JSX.
|
|
93
|
+
*/
|
|
94
|
+
export function useLocaleLabel() {
|
|
95
|
+
const displayName = useDisplayName('language')
|
|
96
|
+
return useCallback((code: string) => formatOption(code, displayName(code) ?? code), [displayName])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
99
|
/**
|
|
100
100
|
* Static text label for a locale code: `EN — English`. Use this in tables
|
|
101
101
|
* and read-only contexts where the picker isn't needed but the same
|
|
@@ -103,16 +103,16 @@ function localeFilter(labelFor: (locale: string) => string) {
|
|
|
103
103
|
* has no entry.
|
|
104
104
|
*/
|
|
105
105
|
export function LocaleLabel({ code }: { code: string }) {
|
|
106
|
-
const
|
|
106
|
+
const labelFor = useLocaleLabel()
|
|
107
107
|
if (!code) return null
|
|
108
|
-
return <>{
|
|
108
|
+
return <>{labelFor(code)}</>
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Picker for one of the current store's
|
|
113
|
-
* default; pass `multiple` for a chips multi-select. Each
|
|
114
|
-
* `CODE — Localized Name` (e.g. `EN — English`),
|
|
115
|
-
*
|
|
112
|
+
* Picker for one of the languages the current store's markets offer.
|
|
113
|
+
* Single-select by default; pass `multiple` for a chips multi-select. Each
|
|
114
|
+
* option reads `CODE — Localized Name` (e.g. `EN — English`), named in the
|
|
115
|
+
* admin UI language via the browser's `Intl.DisplayNames`. Use this anywhere
|
|
116
116
|
* the merchant is choosing a storefront language (market default, supported
|
|
117
117
|
* locales list, customer preference, etc.).
|
|
118
118
|
*
|
|
@@ -124,7 +124,7 @@ export function LocaleLabel({ code }: { code: string }) {
|
|
|
124
124
|
export function LocaleSelect(props: LocaleSelectProps) {
|
|
125
125
|
const { t } = useTranslation()
|
|
126
126
|
const { locales: storeLocales } = useStore()
|
|
127
|
-
const
|
|
127
|
+
const localeLabel = useLocaleLabel()
|
|
128
128
|
|
|
129
129
|
// Union of `props.options` (everything the picker should show) and the
|
|
130
130
|
// currently selected values — so editing an existing record doesn't
|
|
@@ -145,15 +145,14 @@ export function LocaleSelect(props: LocaleSelectProps) {
|
|
|
145
145
|
// per character; a lookup map keeps it to a single pass per list change.
|
|
146
146
|
const labels = useMemo(() => {
|
|
147
147
|
const map = new Map<string, string>()
|
|
148
|
-
for (const locale of items) map.set(locale,
|
|
148
|
+
for (const locale of items) map.set(locale, localeLabel(locale))
|
|
149
149
|
return map
|
|
150
|
-
}, [items,
|
|
150
|
+
}, [items, localeLabel])
|
|
151
151
|
|
|
152
152
|
// `CODE — Name` label for a locale, from the precomputed map (falls back to a
|
|
153
153
|
// direct lookup for a value not in the current option list, e.g. a selected
|
|
154
154
|
// chip the list later dropped).
|
|
155
|
-
const labelFor = (locale: string) =>
|
|
156
|
-
labels.get(locale) ?? formatOption(locale, displayNameFor(locale))
|
|
155
|
+
const labelFor = (locale: string) => labels.get(locale) ?? localeLabel(locale)
|
|
157
156
|
|
|
158
157
|
if (props.multiple) {
|
|
159
158
|
return (
|
|
@@ -183,6 +183,11 @@ function CollapsedDropdown({
|
|
|
183
183
|
<DropdownMenuItem key={subItem.title} asChild>
|
|
184
184
|
<Link to={subItem.url} className="no-underline">
|
|
185
185
|
{subItem.title}
|
|
186
|
+
{subItem.badge && (
|
|
187
|
+
<span className="ms-auto shrink-0 ps-3">
|
|
188
|
+
<subItem.badge />
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
186
191
|
</Link>
|
|
187
192
|
</DropdownMenuItem>
|
|
188
193
|
))}
|
|
@@ -261,9 +266,9 @@ function NavItemContent({
|
|
|
261
266
|
<SidebarMenuSubItem key={subItem.title}>
|
|
262
267
|
<SidebarMenuSubButton asChild isActive={subActive}>
|
|
263
268
|
<Link to={subItem.url}>
|
|
264
|
-
<span>{subItem.title}</span>
|
|
269
|
+
<span className="truncate">{subItem.title}</span>
|
|
265
270
|
{subItem.badge && (
|
|
266
|
-
<span className="ml-auto">
|
|
271
|
+
<span className="ml-auto shrink-0">
|
|
267
272
|
<subItem.badge />
|
|
268
273
|
</span>
|
|
269
274
|
)}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
compareMoney,
|
|
4
|
-
isDecimalString,
|
|
5
|
-
type PreferencePropertySchema,
|
|
6
|
-
type PreferenceSchema,
|
|
7
|
-
} from '@spree/admin-sdk'
|
|
1
|
+
import type { PreferencePropertySchema, PreferenceSchema } from '@spree/admin-sdk'
|
|
2
|
+
import { type CurrencyCode, compareMoney, currencyParts, isDecimalString } from '@spree/core'
|
|
8
3
|
import {
|
|
9
4
|
Button,
|
|
10
5
|
Field,
|
|
@@ -29,7 +24,6 @@ import { PlusIcon, TrashIcon } from '@spree/dashboard-ui/icons'
|
|
|
29
24
|
import { useId, useMemo, useState } from 'react'
|
|
30
25
|
import { useTranslation } from 'react-i18next'
|
|
31
26
|
import { useMoneyLocale } from '../hooks/use-money-locale'
|
|
32
|
-
import { currencyParts } from '../lib/currency-parts'
|
|
33
27
|
import { CountryCombobox, CountryMultiCombobox } from './country-combobox'
|
|
34
28
|
import { CurrencySelect } from './currency-select'
|
|
35
29
|
import { StoreDatePicker } from './store-date-picker'
|
|
@@ -46,6 +40,8 @@ export interface PreferenceFieldDef {
|
|
|
46
40
|
format?: string
|
|
47
41
|
default: unknown
|
|
48
42
|
choices?: string[]
|
|
43
|
+
/** Labels the declaration gave its choices, for values that cannot name themselves (an endpoint URL). */
|
|
44
|
+
choiceLabels?: Record<string, string>
|
|
49
45
|
/** Whether the setting may be left empty (it reads back as null). */
|
|
50
46
|
nullable: boolean
|
|
51
47
|
/** The property's own JSON Schema, for widgets that need more (list items, object properties). */
|
|
@@ -101,6 +97,7 @@ export function preferenceFields(
|
|
|
101
97
|
format: property.format ?? property.items?.format,
|
|
102
98
|
default: property.default ?? null,
|
|
103
99
|
choices: property.enum?.filter((choice): choice is string => typeof choice === 'string'),
|
|
100
|
+
choiceLabels: property['x-spree-choice-labels'],
|
|
104
101
|
nullable: Array.isArray(property.type) && property.type.includes('null'),
|
|
105
102
|
schema: property,
|
|
106
103
|
}))
|
|
@@ -432,12 +429,23 @@ export function PreferenceField({
|
|
|
432
429
|
// the set is what the server validates against, so typing anything
|
|
433
430
|
// else can only fail on save.
|
|
434
431
|
if (field.choices?.length) {
|
|
435
|
-
|
|
432
|
+
// A translation wins where one exists, so the locale files stay in
|
|
433
|
+
// charge of the values that read as words; then the label the
|
|
434
|
+
// declaration supplied, for the ones that do not — a URL cannot serve
|
|
435
|
+
// as its own i18n key, let alone as a label.
|
|
436
|
+
const declared = field.choices.map((choice) => ({
|
|
436
437
|
value: choice,
|
|
437
438
|
label: i18n.exists(`${preferenceKey}_options.${choice}`)
|
|
438
439
|
? t(`${preferenceKey}_options.${choice}`)
|
|
439
|
-
: humanizeKey(choice),
|
|
440
|
+
: (field.choiceLabels?.[choice] ?? humanizeKey(choice)),
|
|
440
441
|
}))
|
|
442
|
+
// A stored value outside the set is offered as its own option rather
|
|
443
|
+
// than dropped, so one written past the picker by an API client is not
|
|
444
|
+
// silently rewritten to the first choice.
|
|
445
|
+
const options =
|
|
446
|
+
typeof value === 'string' && value !== '' && !declared.some((o) => o.value === value)
|
|
447
|
+
? [...declared, { value, label: value }]
|
|
448
|
+
: declared
|
|
441
449
|
|
|
442
450
|
return (
|
|
443
451
|
<Field>
|
|
@@ -109,6 +109,11 @@ export function ResourceCombobox<T extends ComboboxOption>({
|
|
|
109
109
|
// elsewhere (a customer created on another page) never invalidates it.
|
|
110
110
|
// Every search refetches; cached rows for a repeated term still paint first.
|
|
111
111
|
staleTime: 0,
|
|
112
|
+
// Keeps the last results on screen while the next query loads, so the
|
|
113
|
+
// list does not blank on every keystroke. Only within one tenant: the
|
|
114
|
+
// second argument is the query those results came from.
|
|
115
|
+
placeholderData: (previous, previousQuery) =>
|
|
116
|
+
previousQuery?.queryKey[1] === tenantId ? previous : undefined,
|
|
112
117
|
})
|
|
113
118
|
|
|
114
119
|
// Hydrate the currently selected ID into a record so the trigger shows
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Button,
|
|
3
3
|
Checkbox,
|
|
4
|
+
Item,
|
|
5
|
+
ItemActions,
|
|
6
|
+
ItemContent,
|
|
7
|
+
ItemDescription,
|
|
8
|
+
ItemTitle,
|
|
4
9
|
type PaginationMeta,
|
|
5
10
|
SearchInput,
|
|
6
11
|
Sheet,
|
|
@@ -308,11 +313,10 @@ export function ResourcePickerSheet<T extends PickerOption>({
|
|
|
308
313
|
const subtitle = getOptionSubtitle?.(option)
|
|
309
314
|
return (
|
|
310
315
|
<li key={option.id}>
|
|
311
|
-
<
|
|
312
|
-
type="button"
|
|
313
|
-
disabled={inList}
|
|
316
|
+
<Item
|
|
317
|
+
render={<button type="button" disabled={inList} />}
|
|
314
318
|
onClick={() => toggle(option)}
|
|
315
|
-
className="flex
|
|
319
|
+
className="flex-nowrap gap-3 rounded-none px-4 py-2.5 text-left hover:bg-accent/50 disabled:cursor-not-allowed disabled:opacity-60"
|
|
316
320
|
>
|
|
317
321
|
<Checkbox
|
|
318
322
|
checked={checked}
|
|
@@ -320,20 +324,22 @@ export function ResourcePickerSheet<T extends PickerOption>({
|
|
|
320
324
|
className="pointer-events-none"
|
|
321
325
|
/>
|
|
322
326
|
{getOptionImageUrl && <Thumbnail src={imageUrl} size="sm" />}
|
|
323
|
-
<
|
|
324
|
-
<
|
|
327
|
+
<ItemContent className="min-w-0 gap-0">
|
|
328
|
+
<ItemTitle className="block truncate font-normal">
|
|
329
|
+
{getOptionLabel(option)}
|
|
330
|
+
</ItemTitle>
|
|
325
331
|
{subtitle && (
|
|
326
|
-
<
|
|
332
|
+
<ItemDescription className="line-clamp-1 text-xs">
|
|
327
333
|
{subtitle}
|
|
328
|
-
</
|
|
334
|
+
</ItemDescription>
|
|
329
335
|
)}
|
|
330
|
-
</
|
|
336
|
+
</ItemContent>
|
|
331
337
|
{inList && (
|
|
332
|
-
<
|
|
338
|
+
<ItemActions className="shrink-0 text-muted-foreground text-xs">
|
|
333
339
|
{t('admin.resource_picker.already_added')}
|
|
334
|
-
</
|
|
340
|
+
</ItemActions>
|
|
335
341
|
)}
|
|
336
|
-
</
|
|
342
|
+
</Item>
|
|
337
343
|
</li>
|
|
338
344
|
)
|
|
339
345
|
})}
|
|
@@ -358,8 +358,15 @@ function SettingsItem({
|
|
|
358
358
|
<SidebarMenuButton asChild isActive={isActive}>
|
|
359
359
|
<Link to={url} tabIndex={tabIndex}>
|
|
360
360
|
<NavIcon icon={Icon} isActive={isActive} />
|
|
361
|
-
|
|
361
|
+
{/* `truncate` so a long translated label gives way to the badge
|
|
362
|
+
rather than pushing it out of the sidebar. */}
|
|
363
|
+
<span className="truncate">{label}</span>
|
|
362
364
|
{entry.comingSoon && <SidebarMenuBadge className="ms-auto">Soon</SidebarMenuBadge>}
|
|
365
|
+
{entry.badge && (
|
|
366
|
+
<span className="ms-auto shrink-0">
|
|
367
|
+
<entry.badge />
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
363
370
|
</Link>
|
|
364
371
|
</SidebarMenuButton>
|
|
365
372
|
</SidebarMenuItem>
|
package/src/components/slot.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment } from 'react'
|
|
2
|
+
import { FormProvider, type UseFormReturn } from 'react-hook-form'
|
|
2
3
|
import { type SlotAmbientContext, useSlotEntries } from '../lib/slot-registry'
|
|
3
4
|
|
|
4
|
-
interface SlotProps<TContext> {
|
|
5
|
+
export interface SlotProps<TContext> {
|
|
5
6
|
/** Slot name, e.g. "product.form_sidebar" or "order.dropdown". */
|
|
6
7
|
name: string
|
|
7
8
|
/** Slot-specific context passed to every registered component. */
|
|
@@ -48,3 +49,22 @@ export function Slot<TContext extends object = object>({
|
|
|
48
49
|
</>
|
|
49
50
|
)
|
|
50
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A slot inside a form that isn't wrapped in `FormProvider` — a create or edit
|
|
55
|
+
* sheet. Widgets rendered here reach the form with `useHostForm()`, so fields
|
|
56
|
+
* they bind save with the sheet's own submit.
|
|
57
|
+
*/
|
|
58
|
+
export function HostFormSlot<TContext extends object = object>({
|
|
59
|
+
form,
|
|
60
|
+
...slot
|
|
61
|
+
}: SlotProps<TContext> & {
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
form: UseFormReturn<any, any, any>
|
|
64
|
+
}) {
|
|
65
|
+
return (
|
|
66
|
+
<FormProvider {...form}>
|
|
67
|
+
<Slot {...slot} />
|
|
68
|
+
</FormProvider>
|
|
69
|
+
)
|
|
70
|
+
}
|