@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.
Files changed (87) hide show
  1. package/dist/vite/index.js +81 -5
  2. package/package.json +24 -24
  3. package/src/api-client.ts +7 -1
  4. package/src/client.ts +2 -0
  5. package/src/components/address-form-dialog.tsx +75 -28
  6. package/src/components/app-shell.tsx +5 -4
  7. package/src/components/app-sidebar.tsx +2 -1
  8. package/src/components/bulk-action-bar.tsx +6 -5
  9. package/src/components/country-state-fields.tsx +3 -0
  10. package/src/components/currency-select.tsx +6 -4
  11. package/src/components/developer-inspector-drawer.tsx +322 -0
  12. package/src/components/file-upload-field.tsx +64 -37
  13. package/src/components/image-upload-field.tsx +4 -0
  14. package/src/components/import-wizard-dialog.tsx +14 -12
  15. package/src/components/invitation-acceptance.tsx +15 -11
  16. package/src/components/media-editor-dialog.tsx +131 -0
  17. package/src/components/media-picker-dialog.tsx +866 -0
  18. package/src/components/media-picker-sheet.tsx +13 -300
  19. package/src/components/mobile-top-bar.tsx +1 -1
  20. package/src/components/nav-main.tsx +13 -2
  21. package/src/components/page-header.tsx +57 -13
  22. package/src/components/page-tabs.tsx +1 -1
  23. package/src/components/product-stock-summary.tsx +58 -0
  24. package/src/components/read-row.tsx +38 -0
  25. package/src/components/resource-combobox.tsx +1 -1
  26. package/src/components/resource-image-field.tsx +150 -0
  27. package/src/components/resource-picker-sheet.tsx +2 -2
  28. package/src/components/resource-table.tsx +161 -40
  29. package/src/components/section-heading.tsx +2 -2
  30. package/src/components/settings-container.tsx +19 -0
  31. package/src/components/settings-fields.tsx +165 -0
  32. package/src/components/settings-section.tsx +46 -0
  33. package/src/components/settings-sidebar.tsx +35 -18
  34. package/src/components/sheet-top-bar.tsx +7 -8
  35. package/src/components/sidebar-search.tsx +11 -12
  36. package/src/components/sidebar-user.tsx +39 -44
  37. package/src/components/skip-link.tsx +1 -1
  38. package/src/components/store-switcher.tsx +1 -1
  39. package/src/components/table-toolbar.tsx +194 -331
  40. package/src/components/timezone-combobox.tsx +119 -0
  41. package/src/components/top-bar-breadcrumbs.tsx +1 -1
  42. package/src/hooks/use-stock-locations.ts +6 -2
  43. package/src/hooks/use-table-density.ts +50 -0
  44. package/src/index.ts +20 -2
  45. package/src/lib/curl.test.ts +18 -0
  46. package/src/lib/curl.ts +28 -0
  47. package/src/lib/developer-inspector.test.ts +169 -0
  48. package/src/lib/developer-inspector.ts +134 -0
  49. package/src/lib/docs.ts +3 -2
  50. package/src/lib/fulfillment-items.test.ts +51 -0
  51. package/src/lib/fulfillment-items.ts +53 -0
  52. package/src/lib/request-snippets.ts +41 -0
  53. package/src/lib/table-registry.ts +13 -6
  54. package/src/lib/timestamp-columns.tsx +1 -1
  55. package/src/locales/ar.json +128 -14
  56. package/src/locales/de.json +128 -14
  57. package/src/locales/en.json +128 -14
  58. package/src/locales/es.json +128 -14
  59. package/src/locales/fr.json +128 -14
  60. package/src/locales/ja.json +128 -14
  61. package/src/locales/pl.json +130 -14
  62. package/src/locales/vi.json +128 -14
  63. package/src/locales/zh-CN.json +128 -14
  64. package/src/pages/settings-index.tsx +79 -48
  65. package/src/pages/stock-locations.tsx +14 -6
  66. package/src/products/digital-asset-edit-sheet.tsx +1 -1
  67. package/src/products/digital-asset-provider-sheet.tsx +1 -1
  68. package/src/products/digital-assets-card.tsx +7 -7
  69. package/src/products/inventory-section.tsx +2 -2
  70. package/src/products/media-edit-sheet.tsx +136 -133
  71. package/src/products/product-form-cards.tsx +204 -73
  72. package/src/products/product-form-mapping.test.ts +42 -1
  73. package/src/products/product-form-mapping.ts +20 -0
  74. package/src/products/product-schema.ts +8 -0
  75. package/src/products/product-types.ts +2 -0
  76. package/src/products/use-product-form-data.ts +30 -3
  77. package/src/products/variant-edit-sheet.tsx +3 -3
  78. package/src/products/variants-options-builder.tsx +13 -8
  79. package/src/products/variants-section.tsx +196 -4
  80. package/src/providers/auth-provider.tsx +2 -0
  81. package/src/providers/developer-inspector-provider.tsx +145 -0
  82. package/src/providers/json-link-resolver-provider.tsx +27 -0
  83. package/src/sellers/seller-schema.ts +107 -0
  84. package/src/tables/package-types.tsx +3 -3
  85. package/src/tables/stock-locations.tsx +15 -14
  86. package/src/virtual.d.ts +4 -0
  87. 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-sm text-muted-foreground whitespace-nowrap',
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-sm text-muted-foreground whitespace-nowrap',
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-sm text-muted-foreground whitespace-nowrap',
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.fields.name.label'),
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 [dashboardProseMirrorDedupePlugin(), dashboardTailwindSourcePlugin(options), tailwindcss()]
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
- const entryFile = entryPath.startsWith('file:') ? fileURLToPath(entryPath) : entryPath
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
  }