@spree/dashboard-core 6.0.3 → 6.0.4
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/dist/vite/index.js +81 -5
- package/package.json +19 -19
- package/src/api-client.ts +7 -1
- package/src/client.ts +2 -0
- package/src/components/address-form-dialog.tsx +75 -28
- package/src/components/app-shell.tsx +5 -4
- package/src/components/app-sidebar.tsx +2 -1
- package/src/components/bulk-action-bar.tsx +6 -5
- package/src/components/country-state-fields.tsx +3 -0
- package/src/components/currency-select.tsx +6 -4
- package/src/components/developer-inspector-drawer.tsx +322 -0
- package/src/components/file-upload-field.tsx +64 -37
- package/src/components/image-upload-field.tsx +4 -0
- package/src/components/import-wizard-dialog.tsx +14 -12
- package/src/components/invitation-acceptance.tsx +15 -11
- package/src/components/media-editor-dialog.tsx +131 -0
- package/src/components/media-picker-dialog.tsx +866 -0
- package/src/components/media-picker-sheet.tsx +13 -300
- package/src/components/mobile-top-bar.tsx +1 -1
- package/src/components/nav-main.tsx +13 -2
- package/src/components/page-header.tsx +57 -13
- package/src/components/page-tabs.tsx +1 -1
- package/src/components/product-stock-summary.tsx +1 -1
- package/src/components/read-row.tsx +38 -0
- package/src/components/resource-combobox.tsx +1 -1
- package/src/components/resource-image-field.tsx +150 -0
- package/src/components/resource-picker-sheet.tsx +2 -2
- package/src/components/resource-table.tsx +161 -40
- package/src/components/section-heading.tsx +2 -2
- package/src/components/settings-container.tsx +19 -0
- package/src/components/settings-fields.tsx +165 -0
- package/src/components/settings-section.tsx +46 -0
- package/src/components/settings-sidebar.tsx +35 -18
- package/src/components/sheet-top-bar.tsx +7 -8
- package/src/components/sidebar-search.tsx +11 -12
- package/src/components/sidebar-user.tsx +39 -44
- package/src/components/skip-link.tsx +1 -1
- package/src/components/store-switcher.tsx +1 -1
- package/src/components/table-toolbar.tsx +189 -323
- package/src/components/timezone-combobox.tsx +119 -0
- package/src/components/top-bar-breadcrumbs.tsx +1 -1
- package/src/hooks/use-stock-locations.ts +6 -2
- package/src/hooks/use-table-density.ts +50 -0
- package/src/index.ts +19 -2
- package/src/lib/curl.test.ts +18 -0
- package/src/lib/curl.ts +28 -0
- package/src/lib/developer-inspector.test.ts +169 -0
- package/src/lib/developer-inspector.ts +134 -0
- package/src/lib/docs.ts +3 -2
- package/src/lib/fulfillment-items.test.ts +51 -0
- package/src/lib/fulfillment-items.ts +53 -0
- package/src/lib/request-snippets.ts +41 -0
- package/src/lib/table-registry.ts +13 -6
- package/src/lib/timestamp-columns.tsx +1 -1
- package/src/locales/ar.json +120 -14
- package/src/locales/de.json +120 -14
- package/src/locales/en.json +120 -14
- package/src/locales/es.json +120 -14
- package/src/locales/fr.json +120 -14
- package/src/locales/ja.json +120 -14
- package/src/locales/pl.json +120 -14
- package/src/locales/vi.json +120 -14
- package/src/locales/zh-CN.json +120 -14
- package/src/pages/settings-index.tsx +79 -48
- package/src/pages/stock-locations.tsx +14 -6
- package/src/products/digital-asset-edit-sheet.tsx +1 -1
- package/src/products/digital-asset-provider-sheet.tsx +1 -1
- package/src/products/digital-assets-card.tsx +7 -7
- package/src/products/inventory-section.tsx +2 -2
- package/src/products/media-edit-sheet.tsx +136 -133
- package/src/products/product-form-cards.tsx +201 -70
- package/src/products/product-form-mapping.ts +1 -0
- package/src/products/product-schema.ts +8 -0
- package/src/products/product-types.ts +2 -0
- package/src/products/use-product-form-data.ts +30 -3
- package/src/products/variant-edit-sheet.tsx +3 -3
- package/src/products/variants-options-builder.tsx +13 -8
- package/src/products/variants-section.tsx +196 -4
- package/src/providers/auth-provider.tsx +2 -0
- package/src/providers/developer-inspector-provider.tsx +145 -0
- package/src/providers/json-link-resolver-provider.tsx +27 -0
- package/src/sellers/seller-schema.ts +107 -0
- package/src/tables/package-types.tsx +3 -3
- package/src/tables/stock-locations.tsx +15 -14
- package/src/virtual.d.ts +4 -0
- package/src/vite/index.ts +103 -5
package/dist/vite/index.js
CHANGED
|
@@ -5,7 +5,81 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
6
6
|
import { discoverDashboardPlugins } from "./discover.js";
|
|
7
7
|
function spreeDashboardPlugin(options = {}) {
|
|
8
|
-
return [
|
|
8
|
+
return [
|
|
9
|
+
dashboardProseMirrorDedupePlugin(),
|
|
10
|
+
dashboardTailwindSourcePlugin(options),
|
|
11
|
+
tailwindcss(),
|
|
12
|
+
dashboardFontPreloadPlugin(),
|
|
13
|
+
dashboardPackagesPlugin()
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
function dashboardPackagesPlugin() {
|
|
17
|
+
return {
|
|
18
|
+
name: "spree:dashboard-packages",
|
|
19
|
+
config(config) {
|
|
20
|
+
const root = path.resolve(config.root ?? process.cwd());
|
|
21
|
+
return { define: { __SPREE_PACKAGES__: JSON.stringify(installedSpreePackages(root)) } };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function installedSpreePackages(root) {
|
|
26
|
+
const versions = {};
|
|
27
|
+
const visit = (packageRoot, includeDev) => {
|
|
28
|
+
const manifest = readJson(path.join(packageRoot, "package.json"));
|
|
29
|
+
const dependencies = {
|
|
30
|
+
...manifest?.dependencies,
|
|
31
|
+
...manifest?.peerDependencies,
|
|
32
|
+
...includeDev ? manifest?.devDependencies : {}
|
|
33
|
+
};
|
|
34
|
+
const require2 = createRequire(path.join(packageRoot, "package.json"));
|
|
35
|
+
for (const name of Object.keys(dependencies)) {
|
|
36
|
+
if (!name.startsWith("@spree/") || name in versions) continue;
|
|
37
|
+
const dependencyRoot = resolvePackageRoot(name, require2);
|
|
38
|
+
const dependency = dependencyRoot && readJson(path.join(dependencyRoot, "package.json"));
|
|
39
|
+
if (!dependencyRoot || !dependency || dependency.private) continue;
|
|
40
|
+
versions[name] = dependency.version;
|
|
41
|
+
visit(dependencyRoot, false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
visit(root, true);
|
|
45
|
+
return Object.fromEntries(Object.entries(versions).sort(([a], [b]) => a.localeCompare(b)));
|
|
46
|
+
}
|
|
47
|
+
function readJson(file) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const PRELOADED_FONT_FILE = "inter-latin-opsz-normal.woff2";
|
|
55
|
+
function dashboardFontPreloadPlugin() {
|
|
56
|
+
let base = "/";
|
|
57
|
+
return {
|
|
58
|
+
name: "spree:dashboard-font-preload",
|
|
59
|
+
apply: "build",
|
|
60
|
+
configResolved(config) {
|
|
61
|
+
base = config.base;
|
|
62
|
+
},
|
|
63
|
+
transformIndexHtml(_html, ctx) {
|
|
64
|
+
const font = Object.values(ctx.bundle ?? {}).find(
|
|
65
|
+
(file) => file.type === "asset" && file.names.includes(PRELOADED_FONT_FILE)
|
|
66
|
+
);
|
|
67
|
+
if (!font) return;
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
tag: "link",
|
|
71
|
+
attrs: {
|
|
72
|
+
rel: "preload",
|
|
73
|
+
href: `${base}${font.fileName}`,
|
|
74
|
+
as: "font",
|
|
75
|
+
type: "font/woff2",
|
|
76
|
+
crossorigin: ""
|
|
77
|
+
},
|
|
78
|
+
injectTo: "head"
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
};
|
|
9
83
|
}
|
|
10
84
|
function dashboardProseMirrorDedupePlugin() {
|
|
11
85
|
return {
|
|
@@ -129,12 +203,13 @@ function formatPluginErrorOverlay(errors) {
|
|
|
129
203
|
return lines.join("\n");
|
|
130
204
|
}
|
|
131
205
|
function resolvePackageSourceDir(pkg, require2) {
|
|
206
|
+
const packageRoot = resolvePackageRoot(pkg, require2);
|
|
207
|
+
return packageRoot && path.join(packageRoot, "src");
|
|
208
|
+
}
|
|
209
|
+
function resolvePackageRoot(pkg, require2) {
|
|
132
210
|
try {
|
|
133
211
|
const entryPath = require2.resolve(pkg);
|
|
134
|
-
|
|
135
|
-
const packageRoot = findPackageRoot(entryFile);
|
|
136
|
-
if (!packageRoot) return null;
|
|
137
|
-
return path.join(packageRoot, "src");
|
|
212
|
+
return findPackageRoot(entryPath.startsWith("file:") ? fileURLToPath(entryPath) : entryPath);
|
|
138
213
|
} catch {
|
|
139
214
|
return null;
|
|
140
215
|
}
|
|
@@ -151,5 +226,6 @@ function findPackageRoot(fromFile) {
|
|
|
151
226
|
export {
|
|
152
227
|
VIRTUAL_PLUGIN_MODULE_ID,
|
|
153
228
|
generateVirtualPluginModule,
|
|
229
|
+
installedSpreePackages,
|
|
154
230
|
spreeDashboardPlugin
|
|
155
231
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/dashboard-core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "6.0.
|
|
4
|
+
"version": "6.0.4",
|
|
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.",
|
|
6
6
|
"author": "Vendo Connect Inc.",
|
|
7
7
|
"homepage": "https://spreecommerce.org",
|
|
@@ -49,45 +49,45 @@
|
|
|
49
49
|
"LICENSE"
|
|
50
50
|
],
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@spree/admin-sdk": ">=6.0.
|
|
53
|
-
"@spree/dashboard-ui": ">=6.0.
|
|
52
|
+
"@spree/admin-sdk": ">=6.0.4 <7.0.0-0",
|
|
53
|
+
"@spree/dashboard-ui": ">=6.0.4 <7.0.0-0",
|
|
54
54
|
"@tanstack/react-query": "^5.100.9",
|
|
55
55
|
"@tanstack/react-router": "^1.169.2",
|
|
56
|
-
"i18next": "^26.2.0",
|
|
56
|
+
"i18next": "^26.2.0 || ^27.0.0",
|
|
57
57
|
"react": "^19.2.6",
|
|
58
58
|
"react-dom": "^19.2.6",
|
|
59
59
|
"react-hook-form": "^7.75.0",
|
|
60
|
-
"react-i18next": "^17.0.8"
|
|
60
|
+
"react-i18next": "^17.0.8 || ^18.0.0"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@dnd-kit/core": "6.3.1",
|
|
64
64
|
"@dnd-kit/sortable": "10.0.0",
|
|
65
65
|
"@dnd-kit/utilities": "3.2.2",
|
|
66
|
-
"@hookform/resolvers": "^
|
|
67
|
-
"@tailwindcss/vite": "^4.
|
|
68
|
-
"@tanstack/react-hotkeys": "0.
|
|
66
|
+
"@hookform/resolvers": "^7.0.5",
|
|
67
|
+
"@tailwindcss/vite": "^4.3.5",
|
|
68
|
+
"@tanstack/react-hotkeys": "0.23.0",
|
|
69
69
|
"date-fns": "4.4.0",
|
|
70
70
|
"date-fns-tz": "3.2.0",
|
|
71
|
-
"lucide-react": "1.
|
|
71
|
+
"lucide-react": "1.71.0",
|
|
72
72
|
"zod": "^4.6.7",
|
|
73
|
-
"@spree/core": "^6.0.
|
|
73
|
+
"@spree/core": "^6.0.4"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@tanstack/react-query": "^5.
|
|
77
|
-
"@tanstack/react-router": "^1.170.
|
|
76
|
+
"@tanstack/react-query": "^5.107.1",
|
|
77
|
+
"@tanstack/react-router": "^1.170.46",
|
|
78
78
|
"@types/react": "^19.2.14",
|
|
79
79
|
"@types/react-dom": "^19.2.3",
|
|
80
|
-
"i18next": "^
|
|
80
|
+
"i18next": "^27.1.1",
|
|
81
81
|
"react": "^19.3.1",
|
|
82
82
|
"react-dom": "^19.3.1",
|
|
83
|
-
"react-hook-form": "^7.
|
|
84
|
-
"react-i18next": "^
|
|
83
|
+
"react-hook-form": "^7.91.0",
|
|
84
|
+
"react-i18next": "^18.0.1",
|
|
85
85
|
"tsup": "^9.0.0",
|
|
86
86
|
"typescript": "~7.0.2",
|
|
87
|
-
"vite": "^8.3.
|
|
88
|
-
"vitest": "^5.0
|
|
89
|
-
"@spree/
|
|
90
|
-
"@spree/
|
|
87
|
+
"vite": "^8.3.7",
|
|
88
|
+
"vitest": "^5.1.0",
|
|
89
|
+
"@spree/admin-sdk": "^6.0.4",
|
|
90
|
+
"@spree/dashboard-ui": "^6.0.4"
|
|
91
91
|
},
|
|
92
92
|
"scripts": {
|
|
93
93
|
"lint": "biome check src",
|
package/src/api-client.ts
CHANGED
|
@@ -212,7 +212,7 @@ export interface PanelApiClient {
|
|
|
212
212
|
* A download is a bare `fetch`, not an SDK call, so it bypasses whatever
|
|
213
213
|
* the client normally attaches — which on the seller panel is
|
|
214
214
|
* `X-Spree-Seller-Id`, and without it every Seller API request is refused
|
|
215
|
-
* before the action runs
|
|
215
|
+
* before the action runs; on the admin panel, `X-Spree-Store-Id`.
|
|
216
216
|
*/
|
|
217
217
|
downloadHeaders?(): Record<string, string>
|
|
218
218
|
/**
|
|
@@ -265,6 +265,12 @@ export interface PanelApiClient {
|
|
|
265
265
|
*/
|
|
266
266
|
mediaLibrary?: {
|
|
267
267
|
list(params?: Record<string, unknown>): Promise<{ data: PanelMediaRecord[] }>
|
|
268
|
+
/** Adds an uploaded blob to the library, so the picker can offer uploads. */
|
|
269
|
+
create?(params: {
|
|
270
|
+
signed_id: string
|
|
271
|
+
alt?: string
|
|
272
|
+
media_type?: 'image' | 'video'
|
|
273
|
+
}): Promise<PanelMediaRecord>
|
|
268
274
|
}
|
|
269
275
|
/**
|
|
270
276
|
* CSV imports, for whichever catalog this panel manages.
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createAdminClient } from '@spree/admin-sdk'
|
|
2
|
+
import { recordRequest } from './lib/developer-inspector'
|
|
2
3
|
|
|
3
4
|
// `VITE_SPREE_API_URL` controls whether the SDK builds relative or absolute URLs.
|
|
4
5
|
// Three deployment topologies, one code path:
|
|
@@ -22,4 +23,5 @@ const baseUrl = import.meta.env.VITE_SPREE_API_URL || ''
|
|
|
22
23
|
export const adminClient = createAdminClient({
|
|
23
24
|
baseUrl,
|
|
24
25
|
// No initial token — auth-provider bootstraps via /auth/refresh on mount.
|
|
26
|
+
onRequestComplete: recordRequest,
|
|
25
27
|
})
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
SheetFooter,
|
|
17
17
|
SheetHeader,
|
|
18
18
|
SheetTitle,
|
|
19
|
+
useConfirm,
|
|
19
20
|
} from '@spree/dashboard-ui'
|
|
20
21
|
import { useEffect, useRef } from 'react'
|
|
21
22
|
import { Controller, type DefaultValues, useForm } from 'react-hook-form'
|
|
@@ -53,10 +54,7 @@ const addressFormFields = {
|
|
|
53
54
|
.string()
|
|
54
55
|
.trim()
|
|
55
56
|
.min(1, { error: requiredMessage('city') }),
|
|
56
|
-
postal_code: z
|
|
57
|
-
.string()
|
|
58
|
-
.trim()
|
|
59
|
-
.min(1, { error: requiredMessage('address.postal_code') }),
|
|
57
|
+
postal_code: z.string(),
|
|
60
58
|
country_code: countryCodeSchema,
|
|
61
59
|
state_code: z.string(),
|
|
62
60
|
phone: z.string(),
|
|
@@ -99,6 +97,33 @@ const businessSchema = z.object({
|
|
|
99
97
|
|
|
100
98
|
type AddressFormValues = z.infer<typeof personalSchema>
|
|
101
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The fields whose requirement depends on the chosen country. Checked in the
|
|
102
|
+
* schema rather than on submit, so they report alongside every other missing
|
|
103
|
+
* field instead of on a second attempt.
|
|
104
|
+
*/
|
|
105
|
+
function withCountryRules(
|
|
106
|
+
schema: typeof personalSchema | typeof businessSchema,
|
|
107
|
+
rules: { statesRequired: boolean; postalCodeRequired: boolean },
|
|
108
|
+
) {
|
|
109
|
+
return schema.superRefine((values, ctx) => {
|
|
110
|
+
if (rules.statesRequired && !values.state_code.trim()) {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: 'custom',
|
|
113
|
+
path: ['state_code'],
|
|
114
|
+
message: requiredMessage('state_code')(),
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
if (rules.postalCodeRequired && !values.postal_code.trim()) {
|
|
118
|
+
ctx.addIssue({
|
|
119
|
+
code: 'custom',
|
|
120
|
+
path: ['postal_code'],
|
|
121
|
+
message: requiredMessage('address.postal_code')(),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
/** Address rows validate on `state`; every address form binds `state_code`. */
|
|
103
128
|
const ADDRESS_SERVER_FIELD_ALIASES = { state: 'state_code' } as const satisfies Partial<
|
|
104
129
|
Record<string, keyof AddressFormValues>
|
|
@@ -200,15 +225,43 @@ export function AddressFormDialog({
|
|
|
200
225
|
const resolvedTitle = title ?? t('admin.components.address_form_dialog.edit_title')
|
|
201
226
|
// Optional: the seller panel has no store context, so a new address there
|
|
202
227
|
// stays country-empty rather than refusing to render.
|
|
203
|
-
const
|
|
228
|
+
const storeContext = useOptionalStore()
|
|
229
|
+
const storeDefaultCountryCode = storeContext?.defaultCountry
|
|
230
|
+
// Left unmarked rather than marked optional when the store asks for a
|
|
231
|
+
// phone: a stock location never requires one, so "required" would be wrong
|
|
232
|
+
// there.
|
|
233
|
+
const phoneOptional = !storeContext?.store?.address_requires_phone
|
|
204
234
|
const form = useForm<AddressFormValues>({
|
|
205
235
|
defaultValues: buildDefaults(address, storeDefaultCountryCode),
|
|
206
|
-
resolver:
|
|
236
|
+
resolver: (values, context, options) =>
|
|
237
|
+
zodResolver(
|
|
238
|
+
withCountryRules(business ? businessSchema : personalSchema, countryRules.current),
|
|
239
|
+
)(values, context, options),
|
|
207
240
|
})
|
|
208
|
-
const { errors } = form.formState
|
|
241
|
+
const { errors, isDirty } = form.formState
|
|
242
|
+
const confirm = useConfirm()
|
|
243
|
+
// Saving a filled-in address waits for a change. A blank one stays
|
|
244
|
+
// saveable, so pressing Save shows what is still missing.
|
|
245
|
+
const saveNeedsChange = Boolean(address?.address1)
|
|
246
|
+
|
|
247
|
+
// Closing with an edit asks first, whichever way the sheet is closed.
|
|
248
|
+
async function requestClose() {
|
|
249
|
+
if (isDirty && !isPending) {
|
|
250
|
+
const ok = await confirm({
|
|
251
|
+
title: t('admin.actions.discard_changes'),
|
|
252
|
+
message: t('admin.common.unsaved_changes'),
|
|
253
|
+
variant: 'destructive',
|
|
254
|
+
confirmLabel: t('admin.actions.discard'),
|
|
255
|
+
})
|
|
256
|
+
if (!ok) return
|
|
257
|
+
}
|
|
258
|
+
onOpenChange(false)
|
|
259
|
+
}
|
|
209
260
|
|
|
210
261
|
const countryCode = form.watch('country_code')
|
|
211
|
-
const { states, statesRequired } = useCountryStates(countryCode)
|
|
262
|
+
const { states, statesRequired, postalCodeRequired } = useCountryStates(countryCode)
|
|
263
|
+
const countryRules = useRef({ statesRequired, postalCodeRequired })
|
|
264
|
+
countryRules.current = { statesRequired, postalCodeRequired }
|
|
212
265
|
|
|
213
266
|
// The parent keys the dialog on the address id so a fresh instance mounts
|
|
214
267
|
// for each open, but `address` can also stream in async (loaded after the
|
|
@@ -239,14 +292,6 @@ export function AddressFormDialog({
|
|
|
239
292
|
const useStateCombobox = states.length > 0
|
|
240
293
|
|
|
241
294
|
async function onSubmit(values: AddressFormValues) {
|
|
242
|
-
if (statesRequired && !values.state_code.trim()) {
|
|
243
|
-
form.setError('state_code', {
|
|
244
|
-
type: 'manual',
|
|
245
|
-
message: requiredMessage('state_code')(),
|
|
246
|
-
})
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
295
|
try {
|
|
251
296
|
await onSave({
|
|
252
297
|
first_name: values.first_name,
|
|
@@ -285,7 +330,7 @@ export function AddressFormDialog({
|
|
|
285
330
|
}
|
|
286
331
|
|
|
287
332
|
return (
|
|
288
|
-
<Sheet open={open} onOpenChange={(o) =>
|
|
333
|
+
<Sheet open={open} onOpenChange={(o) => (o ? onOpenChange(true) : requestClose())}>
|
|
289
334
|
<SheetContent side="right">
|
|
290
335
|
<SheetHeader>
|
|
291
336
|
<SheetTitle>{resolvedTitle}</SheetTitle>
|
|
@@ -300,14 +345,14 @@ export function AddressFormDialog({
|
|
|
300
345
|
>
|
|
301
346
|
<div className="flex-1 overflow-y-auto p-4">
|
|
302
347
|
{errors.root?.message && (
|
|
303
|
-
<p className="mb-4 text-
|
|
348
|
+
<p className="mb-4 text-base text-destructive" role="alert">
|
|
304
349
|
{errors.root.message}
|
|
305
350
|
</p>
|
|
306
351
|
)}
|
|
307
352
|
<FieldGroup>
|
|
308
353
|
{showLabel && (
|
|
309
354
|
<Field>
|
|
310
|
-
<FieldLabel htmlFor="addr-label">
|
|
355
|
+
<FieldLabel htmlFor="addr-label" optional>
|
|
311
356
|
{t('admin.fields.address.label.label')}
|
|
312
357
|
</FieldLabel>
|
|
313
358
|
<Input
|
|
@@ -366,7 +411,7 @@ export function AddressFormDialog({
|
|
|
366
411
|
<FieldError errors={[errors.address1]} />
|
|
367
412
|
</Field>
|
|
368
413
|
<Field>
|
|
369
|
-
<FieldLabel htmlFor="addr-a2">
|
|
414
|
+
<FieldLabel htmlFor="addr-a2" optional>
|
|
370
415
|
{t('admin.fields.address.address2.label')}
|
|
371
416
|
</FieldLabel>
|
|
372
417
|
<Input
|
|
@@ -410,7 +455,7 @@ export function AddressFormDialog({
|
|
|
410
455
|
</Field>
|
|
411
456
|
{useStateCombobox ? (
|
|
412
457
|
<Field>
|
|
413
|
-
<FieldLabel htmlFor="addr-state">
|
|
458
|
+
<FieldLabel htmlFor="addr-state" optional={!statesRequired}>
|
|
414
459
|
{t('admin.fields.state_code.label')}
|
|
415
460
|
</FieldLabel>
|
|
416
461
|
<Controller
|
|
@@ -432,7 +477,7 @@ export function AddressFormDialog({
|
|
|
432
477
|
</Field>
|
|
433
478
|
) : (
|
|
434
479
|
<Field>
|
|
435
|
-
<FieldLabel htmlFor="addr-state-abbr">
|
|
480
|
+
<FieldLabel htmlFor="addr-state-abbr" optional={!statesRequired}>
|
|
436
481
|
{t('admin.fields.state_code.label')}
|
|
437
482
|
</FieldLabel>
|
|
438
483
|
<Input
|
|
@@ -445,7 +490,7 @@ export function AddressFormDialog({
|
|
|
445
490
|
)}
|
|
446
491
|
</div>
|
|
447
492
|
<Field>
|
|
448
|
-
<FieldLabel htmlFor="addr-zip">
|
|
493
|
+
<FieldLabel htmlFor="addr-zip" optional={!postalCodeRequired}>
|
|
449
494
|
{t('admin.fields.address.postal_code.label')}
|
|
450
495
|
</FieldLabel>
|
|
451
496
|
<Input
|
|
@@ -456,7 +501,9 @@ export function AddressFormDialog({
|
|
|
456
501
|
<FieldError errors={[errors.postal_code]} />
|
|
457
502
|
</Field>
|
|
458
503
|
<Field>
|
|
459
|
-
<FieldLabel htmlFor="addr-phone"
|
|
504
|
+
<FieldLabel htmlFor="addr-phone" optional={phoneOptional}>
|
|
505
|
+
{t('admin.fields.phone.label')}
|
|
506
|
+
</FieldLabel>
|
|
460
507
|
<Input
|
|
461
508
|
id="addr-phone"
|
|
462
509
|
aria-invalid={!!errors.phone || undefined}
|
|
@@ -473,7 +520,7 @@ export function AddressFormDialog({
|
|
|
473
520
|
render={({ field }) => (
|
|
474
521
|
<label
|
|
475
522
|
htmlFor="addr-default-billing"
|
|
476
|
-
className="flex cursor-pointer items-center gap-2 text-
|
|
523
|
+
className="flex cursor-pointer items-center gap-2 text-base"
|
|
477
524
|
>
|
|
478
525
|
<Checkbox
|
|
479
526
|
id="addr-default-billing"
|
|
@@ -492,7 +539,7 @@ export function AddressFormDialog({
|
|
|
492
539
|
render={({ field }) => (
|
|
493
540
|
<label
|
|
494
541
|
htmlFor="addr-default-shipping"
|
|
495
|
-
className="flex cursor-pointer items-center gap-2 text-
|
|
542
|
+
className="flex cursor-pointer items-center gap-2 text-base"
|
|
496
543
|
>
|
|
497
544
|
<Checkbox
|
|
498
545
|
id="addr-default-shipping"
|
|
@@ -509,10 +556,10 @@ export function AddressFormDialog({
|
|
|
509
556
|
</FieldGroup>
|
|
510
557
|
</div>
|
|
511
558
|
<SheetFooter>
|
|
512
|
-
<Button type="button" variant="outline" onClick={
|
|
559
|
+
<Button type="button" variant="outline" onClick={requestClose}>
|
|
513
560
|
{t('admin.actions.cancel')}
|
|
514
561
|
</Button>
|
|
515
|
-
<Button type="submit" disabled={isPending}>
|
|
562
|
+
<Button type="submit" disabled={isPending || (saveNeedsChange && !isDirty)}>
|
|
516
563
|
{isPending ? t('admin.actions.saving') : t('admin.actions.save')}
|
|
517
564
|
</Button>
|
|
518
565
|
</SheetFooter>
|
|
@@ -103,10 +103,11 @@ export function AppShell({
|
|
|
103
103
|
a scroll container of its own to pin inside, so it can never ride up
|
|
104
104
|
over the rail's switcher.
|
|
105
105
|
|
|
106
|
-
`flex-row` so the
|
|
107
|
-
|
|
106
|
+
`flex-row` so the page's own columns sit side by side. */}
|
|
107
|
+
{/* The settings nav sits in the chrome between the rail and the sheet,
|
|
108
|
+
on the sidebar's ground, so the sheet holds only the page. */}
|
|
109
|
+
<SettingsSidebar open={inSettings} tenantId={tenantId} />
|
|
108
110
|
<SidebarInset className="min-h-0 flex-row overflow-hidden">
|
|
109
|
-
<SettingsSidebar open={inSettings} tenantId={tenantId} />
|
|
110
111
|
{/* `tabIndex={-1}`: Page Up/Down, Home and End act on the focused
|
|
111
112
|
element, and this is the element that scrolls. Without it focus
|
|
112
113
|
stays on <body>, which has nothing to scroll now that the sheet
|
|
@@ -138,7 +139,7 @@ export function AppShell({
|
|
|
138
139
|
<div
|
|
139
140
|
ref={scrollerRef}
|
|
140
141
|
tabIndex={-1}
|
|
141
|
-
className="themed-scrollbar flex min-w-0 flex-1 flex-col overflow-y-auto
|
|
142
|
+
className="themed-scrollbar flex min-w-0 flex-1 flex-col overflow-y-auto outline-none"
|
|
142
143
|
>
|
|
143
144
|
<MobileTopBar
|
|
144
145
|
uiLocales={uiLocales}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '@spree/dashboard-ui'
|
|
13
13
|
import { PackageIcon } from '@spree/dashboard-ui/icons'
|
|
14
14
|
import { useParams } from '@tanstack/react-router'
|
|
15
|
+
import type { TFunction } from 'i18next'
|
|
15
16
|
import type { ComponentProps, ReactNode } from 'react'
|
|
16
17
|
import { useAuth } from '../hooks/use-auth'
|
|
17
18
|
import { primarySidebarSide, useTranslation } from '../lib/i18n'
|
|
@@ -28,7 +29,7 @@ import { StoreSwitcher } from './store-switcher'
|
|
|
28
29
|
* `labelKey` is resolved here rather than at registration so labels follow a
|
|
29
30
|
* language change — a literal `label` is frozen at import time.
|
|
30
31
|
*/
|
|
31
|
-
function entryToNavItem(entry: NavEntry, tenantId: string, t:
|
|
32
|
+
function entryToNavItem(entry: NavEntry, tenantId: string, t: TFunction): NavItem {
|
|
32
33
|
const pathFor = (path: string) => (path === '/' ? `/${tenantId}` : `/${tenantId}${path}`)
|
|
33
34
|
return {
|
|
34
35
|
title: resolveNavLabel(entry, t),
|
|
@@ -328,7 +328,7 @@ function BulkActionBarLayout({
|
|
|
328
328
|
{/* No clear button: the bar sits beside the header's select-all
|
|
329
329
|
checkbox, which already clears the selection. */}
|
|
330
330
|
<div ref={leadingRef} className="flex items-center gap-0.5">
|
|
331
|
-
<span className="whitespace-nowrap px-1.5 text-
|
|
331
|
+
<span className="whitespace-nowrap px-1.5 text-base">
|
|
332
332
|
{t('admin.components.bulk_action_bar.selected', { count })}
|
|
333
333
|
</span>
|
|
334
334
|
<div className="mx-1 h-5 w-px bg-border" aria-hidden />
|
|
@@ -351,6 +351,7 @@ function BulkActionBarLayout({
|
|
|
351
351
|
variant="ghost"
|
|
352
352
|
size="icon-sm"
|
|
353
353
|
aria-label={t('admin.actions.more_actions')}
|
|
354
|
+
className="hover:bg-accent-strong aria-expanded:bg-accent-strong"
|
|
354
355
|
>
|
|
355
356
|
<EllipsisVerticalIcon className="size-4" />
|
|
356
357
|
</Button>
|
|
@@ -424,10 +425,10 @@ function BulkActionButton({
|
|
|
424
425
|
disabled={disabled}
|
|
425
426
|
tabIndex={tabIndex}
|
|
426
427
|
// `shrink-0` so the zero-width measuring layer can't squeeze these and
|
|
427
|
-
// report a width smaller than the button really renders at. Hover
|
|
428
|
-
// ghost variant's
|
|
429
|
-
//
|
|
430
|
-
className="shrink-0 gap-1.5 whitespace-nowrap px-2"
|
|
428
|
+
// report a width smaller than the button really renders at. Hover one
|
|
429
|
+
// step past the ghost variant's `bg-accent`, which is barely apart from
|
|
430
|
+
// the bar's own grey.
|
|
431
|
+
className="shrink-0 gap-1.5 whitespace-nowrap px-2 hover:bg-accent-strong aria-expanded:bg-accent-strong"
|
|
431
432
|
>
|
|
432
433
|
{action.icon}
|
|
433
434
|
{action.label}
|
|
@@ -29,6 +29,9 @@ export function useCountryStates(countryCode: string | null | undefined) {
|
|
|
29
29
|
return {
|
|
30
30
|
states: ((country?.states ?? []) as StateOption[]).filter((s) => Boolean(s.abbr)),
|
|
31
31
|
statesRequired: country?.states_required ?? false,
|
|
32
|
+
// Assumed required until the country list loads, as the server does for
|
|
33
|
+
// an address without a country.
|
|
34
|
+
postalCodeRequired: country?.zipcode_required ?? true,
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ComboboxList,
|
|
9
9
|
ComboboxSearch,
|
|
10
10
|
ComboboxTriggerPlaceholder,
|
|
11
|
+
CurrencyLabel,
|
|
11
12
|
} from '@spree/dashboard-ui'
|
|
12
13
|
import { useMemo, useState } from 'react'
|
|
13
14
|
import { useTranslation } from 'react-i18next'
|
|
@@ -58,8 +59,9 @@ interface CurrencySelectProps {
|
|
|
58
59
|
* current store's markets (the default market's first), so callers
|
|
59
60
|
* choosing among already-configured currencies don't have to wire anything up.
|
|
60
61
|
* Pass `options` (e.g. `ALL_CURRENCY_CODES`) when the merchant may pick any
|
|
61
|
-
* currency, such as a market's currency. Each option
|
|
62
|
-
* (
|
|
62
|
+
* currency, such as a market's currency. Each option shows the currency's
|
|
63
|
+
* symbol, code and name (`[$] USD US Dollar`), the name localized to the admin
|
|
64
|
+
* UI language.
|
|
63
65
|
*
|
|
64
66
|
* Always searchable, whatever the list length. The picker used to fall back to
|
|
65
67
|
* a plain `<Select>` under a dozen options, which meant the same field behaved
|
|
@@ -158,7 +160,7 @@ export function CurrencySelect({
|
|
|
158
160
|
aria-invalid={invalid || undefined}
|
|
159
161
|
>
|
|
160
162
|
{value ? (
|
|
161
|
-
<
|
|
163
|
+
<CurrencyLabel code={value} name={displayNameFor(value)} />
|
|
162
164
|
) : (
|
|
163
165
|
<ComboboxTriggerPlaceholder>
|
|
164
166
|
{t('admin.components.currency_select.placeholder')}
|
|
@@ -175,7 +177,7 @@ export function CurrencySelect({
|
|
|
175
177
|
<ComboboxList>
|
|
176
178
|
{(code: CurrencyCode) => (
|
|
177
179
|
<ComboboxItem key={code} value={code}>
|
|
178
|
-
{
|
|
180
|
+
<CurrencyLabel code={code} name={displayNameFor(code)} />
|
|
179
181
|
</ComboboxItem>
|
|
180
182
|
)}
|
|
181
183
|
</ComboboxList>
|