@spree/dashboard-core 6.0.2 → 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 +24 -24
- 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 +58 -0
- 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 +194 -331
- 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 +20 -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 +128 -14
- package/src/locales/de.json +128 -14
- package/src/locales/en.json +128 -14
- package/src/locales/es.json +128 -14
- package/src/locales/fr.json +128 -14
- package/src/locales/ja.json +128 -14
- package/src/locales/pl.json +130 -14
- package/src/locales/vi.json +128 -14
- package/src/locales/zh-CN.json +128 -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 +204 -73
- package/src/products/product-form-mapping.test.ts +42 -1
- package/src/products/product-form-mapping.ts +20 -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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Seller, SellerCreateParams } from '@spree/admin-sdk'
|
|
2
|
+
import { PAYOUT_SCHEDULE_INTERVALS, TAX_REMITTANCES } from '@spree/core'
|
|
3
|
+
import { requiredMessage } from '@spree/dashboard-ui'
|
|
4
|
+
import { z } from 'zod/v4'
|
|
5
|
+
import { blankToNull } from '../lib/form-mappers'
|
|
6
|
+
|
|
7
|
+
export const sellerFormSchema = z.object({
|
|
8
|
+
name: z
|
|
9
|
+
.string()
|
|
10
|
+
.trim()
|
|
11
|
+
.min(1, { error: requiredMessage('name') }),
|
|
12
|
+
slug: z.string().trim().optional(),
|
|
13
|
+
contact_email: z.email().or(z.literal('')).optional(),
|
|
14
|
+
billing_email: z.email().or(z.literal('')).optional(),
|
|
15
|
+
about: z.string().optional(),
|
|
16
|
+
tax_remittance: z.enum(TAX_REMITTANCES).optional(),
|
|
17
|
+
payouts_schedule_interval: z.enum(PAYOUT_SCHEDULE_INTERVALS).or(z.literal('')).optional(),
|
|
18
|
+
minimum_payout_amount: z.string().trim().optional(),
|
|
19
|
+
holiday_mode_until: z.string().optional(),
|
|
20
|
+
// Three-state image fields, one triple per attachment. See ResourceImageField.
|
|
21
|
+
logo_signed_id: z.string().nullable(),
|
|
22
|
+
logo_preview_url: z.string().nullable(),
|
|
23
|
+
logo_cleared: z.boolean(),
|
|
24
|
+
square_logo_signed_id: z.string().nullable(),
|
|
25
|
+
square_logo_preview_url: z.string().nullable(),
|
|
26
|
+
square_logo_cleared: z.boolean(),
|
|
27
|
+
cover_photo_signed_id: z.string().nullable(),
|
|
28
|
+
cover_photo_preview_url: z.string().nullable(),
|
|
29
|
+
cover_photo_cleared: z.boolean(),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export type SellerFormValues = z.infer<typeof sellerFormSchema>
|
|
33
|
+
|
|
34
|
+
export const SELLER_DEFAULTS: SellerFormValues = {
|
|
35
|
+
name: '',
|
|
36
|
+
slug: '',
|
|
37
|
+
contact_email: '',
|
|
38
|
+
billing_email: '',
|
|
39
|
+
about: '',
|
|
40
|
+
tax_remittance: 'seller',
|
|
41
|
+
payouts_schedule_interval: '',
|
|
42
|
+
minimum_payout_amount: '',
|
|
43
|
+
holiday_mode_until: '',
|
|
44
|
+
logo_signed_id: null,
|
|
45
|
+
logo_preview_url: null,
|
|
46
|
+
logo_cleared: false,
|
|
47
|
+
square_logo_signed_id: null,
|
|
48
|
+
square_logo_preview_url: null,
|
|
49
|
+
square_logo_cleared: false,
|
|
50
|
+
cover_photo_signed_id: null,
|
|
51
|
+
cover_photo_preview_url: null,
|
|
52
|
+
cover_photo_cleared: false,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What the form reads from a seller. Structural, so the seller panel's own
|
|
57
|
+
* profile — the same record under another API — seeds it too.
|
|
58
|
+
*/
|
|
59
|
+
export type SellerFormSource = Pick<
|
|
60
|
+
Seller,
|
|
61
|
+
| 'name'
|
|
62
|
+
| 'slug'
|
|
63
|
+
| 'contact_email'
|
|
64
|
+
| 'billing_email'
|
|
65
|
+
| 'about_html'
|
|
66
|
+
| 'tax_remittance'
|
|
67
|
+
| 'payouts_schedule_interval'
|
|
68
|
+
| 'minimum_payout_amount'
|
|
69
|
+
| 'holiday_mode_until'
|
|
70
|
+
>
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The form seeded from a saved seller. Images start as the empty triple: the
|
|
74
|
+
* saved ones are shown through the field's `serverUrl`, and the triple only
|
|
75
|
+
* carries what the operator changes this time.
|
|
76
|
+
*/
|
|
77
|
+
export function sellerFormValues(seller: SellerFormSource): SellerFormValues {
|
|
78
|
+
return {
|
|
79
|
+
...SELLER_DEFAULTS,
|
|
80
|
+
name: seller.name,
|
|
81
|
+
slug: seller.slug,
|
|
82
|
+
contact_email: seller.contact_email ?? '',
|
|
83
|
+
billing_email: seller.billing_email ?? '',
|
|
84
|
+
about: seller.about_html ?? '',
|
|
85
|
+
tax_remittance: (seller.tax_remittance ?? 'seller') as SellerFormValues['tax_remittance'],
|
|
86
|
+
payouts_schedule_interval: (seller.payouts_schedule_interval ??
|
|
87
|
+
'') as SellerFormValues['payouts_schedule_interval'],
|
|
88
|
+
minimum_payout_amount: seller.minimum_payout_amount ?? '',
|
|
89
|
+
holiday_mode_until: seller.holiday_mode_until ?? '',
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function sellerValuesToParams(values: SellerFormValues): SellerCreateParams {
|
|
94
|
+
return {
|
|
95
|
+
name: values.name,
|
|
96
|
+
slug: blankToNull(values.slug) ?? undefined,
|
|
97
|
+
contact_email: blankToNull(values.contact_email),
|
|
98
|
+
billing_email: blankToNull(values.billing_email),
|
|
99
|
+
about: blankToNull(values.about),
|
|
100
|
+
tax_remittance: values.tax_remittance,
|
|
101
|
+
payouts_schedule_interval: blankToNull(values.payouts_schedule_interval) as
|
|
102
|
+
| SellerCreateParams['payouts_schedule_interval']
|
|
103
|
+
| null,
|
|
104
|
+
minimum_payout_amount: blankToNull(values.minimum_payout_amount),
|
|
105
|
+
holiday_mode_until: blankToNull(values.holiday_mode_until),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -45,7 +45,7 @@ defineTable<PanelPackageType>('package-types', {
|
|
|
45
45
|
key: 'dimensions',
|
|
46
46
|
label: i18n.t('admin.package_types.dimensions_column'),
|
|
47
47
|
default: true,
|
|
48
|
-
className: 'text-
|
|
48
|
+
className: 'text-base text-muted-foreground whitespace-nowrap',
|
|
49
49
|
// Three numbers mean nothing without the unit they were measured in —
|
|
50
50
|
// the same figures read as inches or centimetres differ sixteenfold by
|
|
51
51
|
// volume, which is what the freight tiers compare.
|
|
@@ -57,7 +57,7 @@ defineTable<PanelPackageType>('package-types', {
|
|
|
57
57
|
{
|
|
58
58
|
key: 'volume',
|
|
59
59
|
label: i18n.t('admin.package_types.volume_column'),
|
|
60
|
-
className: 'text-
|
|
60
|
+
className: 'text-base text-muted-foreground whitespace-nowrap',
|
|
61
61
|
render: (packageType) =>
|
|
62
62
|
packageType.volume ? `${Number(packageType.volume).toFixed(3)} m³` : '—',
|
|
63
63
|
},
|
|
@@ -67,7 +67,7 @@ defineTable<PanelPackageType>('package-types', {
|
|
|
67
67
|
sortable: true,
|
|
68
68
|
filterable: true,
|
|
69
69
|
filterType: 'date',
|
|
70
|
-
className: 'text-
|
|
70
|
+
className: 'text-base text-muted-foreground whitespace-nowrap',
|
|
71
71
|
render: (packageType) => <RelativeTime iso={packageType.created_at} />,
|
|
72
72
|
},
|
|
73
73
|
],
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { STOCK_LOCATION_KINDS } from '@spree/core'
|
|
1
|
+
import { intlDisplayName, STOCK_LOCATION_KINDS } from '@spree/core'
|
|
2
2
|
import { ActiveBadge, ResourceNameCell } from '@spree/dashboard-ui'
|
|
3
3
|
import { WarehouseIcon } from '@spree/dashboard-ui/icons'
|
|
4
4
|
import i18n from 'i18next'
|
|
5
5
|
import type { PanelStockLocation } from '../api-client'
|
|
6
6
|
import { defineTable } from '../lib/table-registry'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Where a location is, on one line: street, city, state and country. Sits
|
|
10
|
+
* under the name rather than in a column of its own, since it is what tells
|
|
11
|
+
* two warehouses of a similar name apart.
|
|
12
|
+
*/
|
|
13
|
+
function addressLine(sl: PanelStockLocation): string {
|
|
14
|
+
const country = sl.country_code
|
|
15
|
+
? (intlDisplayName('region', i18n.language, sl.country_code) ?? sl.country_code)
|
|
16
|
+
: null
|
|
17
|
+
return [sl.address1, sl.city, sl.state_text || sl.state_code, country].filter(Boolean).join(', ')
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
defineTable<PanelStockLocation>('stock-locations', {
|
|
9
21
|
docsPath: 'settings/locations',
|
|
10
22
|
description: i18n.t('admin.stock_locations.description'),
|
|
@@ -17,7 +29,7 @@ defineTable<PanelStockLocation>('stock-locations', {
|
|
|
17
29
|
columns: [
|
|
18
30
|
{
|
|
19
31
|
key: 'name',
|
|
20
|
-
label: i18n.t('admin.
|
|
32
|
+
label: i18n.t('admin.stock_locations.columns.location'),
|
|
21
33
|
sortable: true,
|
|
22
34
|
default: true,
|
|
23
35
|
render: (sl) => (
|
|
@@ -25,7 +37,7 @@ defineTable<PanelStockLocation>('stock-locations', {
|
|
|
25
37
|
id={sl.id}
|
|
26
38
|
dataAttr="data-stock-location-id"
|
|
27
39
|
name={sl.name}
|
|
28
|
-
secondary={sl.admin_name}
|
|
40
|
+
secondary={[sl.admin_name, addressLine(sl)].filter(Boolean).join(' · ')}
|
|
29
41
|
/>
|
|
30
42
|
),
|
|
31
43
|
},
|
|
@@ -47,17 +59,6 @@ defineTable<PanelStockLocation>('stock-locations', {
|
|
|
47
59
|
return <span className="capitalize">{label}</span>
|
|
48
60
|
},
|
|
49
61
|
},
|
|
50
|
-
{
|
|
51
|
-
key: 'city',
|
|
52
|
-
label: i18n.t('admin.stock_locations.columns.location'),
|
|
53
|
-
default: true,
|
|
54
|
-
render: (sl) => {
|
|
55
|
-
const parts = [sl.city, sl.state_text || sl.state_code, sl.country_code]
|
|
56
|
-
.filter(Boolean)
|
|
57
|
-
.join(', ')
|
|
58
|
-
return parts || '—'
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
62
|
{
|
|
62
63
|
key: 'pickup_enabled',
|
|
63
64
|
label: i18n.t('admin.stock_locations.columns.pickup'),
|
package/src/virtual.d.ts
CHANGED
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
// imports @spree/dashboard-core can `import 'virtual:spree-dashboard-plugins'`
|
|
4
4
|
// without its own shim.
|
|
5
5
|
declare module 'virtual:spree-dashboard-plugins'
|
|
6
|
+
|
|
7
|
+
// The host's installed `@spree/*` versions, defined by `spreeDashboardPlugin()`.
|
|
8
|
+
// Undefined in a build without the plugin, so read it behind a typeof check.
|
|
9
|
+
declare const __SPREE_PACKAGES__: Record<string, string> | undefined
|
package/src/vite/index.ts
CHANGED
|
@@ -98,7 +98,103 @@ export interface SpreeDashboardPluginOptions {
|
|
|
98
98
|
* })
|
|
99
99
|
*/
|
|
100
100
|
export function spreeDashboardPlugin(options: SpreeDashboardPluginOptions = {}): PluginOption[] {
|
|
101
|
-
return [
|
|
101
|
+
return [
|
|
102
|
+
dashboardProseMirrorDedupePlugin(),
|
|
103
|
+
dashboardTailwindSourcePlugin(options),
|
|
104
|
+
tailwindcss(),
|
|
105
|
+
dashboardFontPreloadPlugin(),
|
|
106
|
+
dashboardPackagesPlugin(),
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Bakes the host's installed `@spree/*` package versions into the build as
|
|
112
|
+
* `__SPREE_PACKAGES__`, so the diagnostics report can name the dashboard a
|
|
113
|
+
* merchant actually deployed, which the server has no way to know.
|
|
114
|
+
*/
|
|
115
|
+
function dashboardPackagesPlugin(): Plugin {
|
|
116
|
+
return {
|
|
117
|
+
name: 'spree:dashboard-packages',
|
|
118
|
+
config(config) {
|
|
119
|
+
const root = path.resolve(config.root ?? process.cwd())
|
|
120
|
+
return { define: { __SPREE_PACKAGES__: JSON.stringify(installedSpreePackages(root)) } }
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `@spree/*` packages the host installs, directly or through another Spree
|
|
127
|
+
* package (`@spree/core` comes in with the dashboard), by installed version.
|
|
128
|
+
* Exported for tests.
|
|
129
|
+
*/
|
|
130
|
+
export function installedSpreePackages(root: string): Record<string, string> {
|
|
131
|
+
const versions: Record<string, string> = {}
|
|
132
|
+
const visit = (packageRoot: string, includeDev: boolean) => {
|
|
133
|
+
const manifest = readJson(path.join(packageRoot, 'package.json'))
|
|
134
|
+
const dependencies = {
|
|
135
|
+
...manifest?.dependencies,
|
|
136
|
+
...manifest?.peerDependencies,
|
|
137
|
+
...(includeDev ? manifest?.devDependencies : {}),
|
|
138
|
+
}
|
|
139
|
+
const require = createRequire(path.join(packageRoot, 'package.json'))
|
|
140
|
+
for (const name of Object.keys(dependencies)) {
|
|
141
|
+
if (!name.startsWith('@spree/') || name in versions) continue
|
|
142
|
+
const dependencyRoot = resolvePackageRoot(name, require)
|
|
143
|
+
const dependency = dependencyRoot && readJson(path.join(dependencyRoot, 'package.json'))
|
|
144
|
+
if (!dependencyRoot || !dependency || dependency.private) continue
|
|
145
|
+
versions[name] = dependency.version
|
|
146
|
+
visit(dependencyRoot, false)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
visit(root, true)
|
|
150
|
+
return Object.fromEntries(Object.entries(versions).sort(([a], [b]) => a.localeCompare(b)))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readJson(file: string): Record<string, any> | null {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
156
|
+
} catch {
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The Latin subset of the dashboard's sans font, which nearly every page renders. */
|
|
162
|
+
const PRELOADED_FONT_FILE = 'inter-latin-opsz-normal.woff2'
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Preloads the Latin sans font in production builds. The font is otherwise
|
|
166
|
+
* discovered only after the CSS is downloaded and parsed, so the first paint
|
|
167
|
+
* falls back to the system font. Hosts that replace the font emit no such
|
|
168
|
+
* file and get no tag.
|
|
169
|
+
*/
|
|
170
|
+
function dashboardFontPreloadPlugin(): Plugin {
|
|
171
|
+
let base = '/'
|
|
172
|
+
return {
|
|
173
|
+
name: 'spree:dashboard-font-preload',
|
|
174
|
+
apply: 'build',
|
|
175
|
+
configResolved(config) {
|
|
176
|
+
base = config.base
|
|
177
|
+
},
|
|
178
|
+
transformIndexHtml(_html, ctx) {
|
|
179
|
+
const font = Object.values(ctx.bundle ?? {}).find(
|
|
180
|
+
(file) => file.type === 'asset' && file.names.includes(PRELOADED_FONT_FILE),
|
|
181
|
+
)
|
|
182
|
+
if (!font) return
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
tag: 'link',
|
|
186
|
+
attrs: {
|
|
187
|
+
rel: 'preload',
|
|
188
|
+
href: `${base}${font.fileName}`,
|
|
189
|
+
as: 'font',
|
|
190
|
+
type: 'font/woff2',
|
|
191
|
+
crossorigin: '',
|
|
192
|
+
},
|
|
193
|
+
injectTo: 'head',
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
}
|
|
102
198
|
}
|
|
103
199
|
|
|
104
200
|
/**
|
|
@@ -280,12 +376,14 @@ function formatPluginErrorOverlay(errors: PluginError[]): string {
|
|
|
280
376
|
* including ours, don't list it there.
|
|
281
377
|
*/
|
|
282
378
|
function resolvePackageSourceDir(pkg: string, require: NodeJS.Require): string | null {
|
|
379
|
+
const packageRoot = resolvePackageRoot(pkg, require)
|
|
380
|
+
return packageRoot && path.join(packageRoot, 'src')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function resolvePackageRoot(pkg: string, require: NodeJS.Require): string | null {
|
|
283
384
|
try {
|
|
284
385
|
const entryPath = require.resolve(pkg)
|
|
285
|
-
|
|
286
|
-
const packageRoot = findPackageRoot(entryFile)
|
|
287
|
-
if (!packageRoot) return null
|
|
288
|
-
return path.join(packageRoot, 'src')
|
|
386
|
+
return findPackageRoot(entryPath.startsWith('file:') ? fileURLToPath(entryPath) : entryPath)
|
|
289
387
|
} catch {
|
|
290
388
|
return null
|
|
291
389
|
}
|