@spree/seller-dashboard 1.0.0-rc.6 → 6.0.0-rc.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/seller-dashboard",
3
- "version": "1.0.0-rc.6",
3
+ "version": "6.0.0-rc.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Spree marketplace seller panel — the seller-facing counterpart to the admin dashboard.",
@@ -36,48 +36,48 @@
36
36
  "LICENSE"
37
37
  ],
38
38
  "dependencies": {
39
- "@hookform/resolvers": "^5.2.2",
39
+ "@hookform/resolvers": "^6.0.0",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/sortable": "^10.0.0",
42
42
  "@dnd-kit/utilities": "^3.2.2",
43
- "@tanstack/router-plugin": "^1.168.46",
43
+ "@tanstack/router-plugin": "^1.168.47",
44
44
  "@tanstack/virtual-file-routes": "^1.162.0",
45
- "lucide-react": "^1.58.0",
45
+ "lucide-react": "^1.60.0",
46
46
  "zod": "^4.6.7",
47
- "@spree/seller-sdk": "1.0.0-rc.5",
48
- "@spree/core": "^0.2.0"
47
+ "@spree/core": "^6.0.0-rc.7",
48
+ "@spree/seller-sdk": "6.0.0-rc.7"
49
49
  },
50
50
  "peerDependencies": {
51
- "@tanstack/react-query": "^5.104.3",
52
- "@tanstack/react-router": "^1.170.44",
53
- "i18next": "^26.4.7",
51
+ "@tanstack/react-query": "^5.104.5",
52
+ "@tanstack/react-router": "^1.170.45",
53
+ "i18next": "^26.5.3",
54
54
  "react": "^19.3.1",
55
55
  "react-dom": "^19.3.1",
56
56
  "react-hook-form": "^7.90.0",
57
- "react-i18next": "^17.0.20",
58
- "@spree/admin-sdk": "^1.0.0-rc.5",
59
- "@spree/dashboard-core": "^1.0.0-rc.5",
60
- "@spree/dashboard-ui": "^1.0.0-rc.5"
57
+ "react-i18next": "^17.1.0",
58
+ "@spree/admin-sdk": "^6.0.0-rc.7",
59
+ "@spree/dashboard-core": "^6.0.0-rc.7",
60
+ "@spree/dashboard-ui": "^6.0.0-rc.7"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@biomejs/biome": "^2.4.14",
64
- "@tanstack/react-query": "^5.104.3",
65
- "@tanstack/react-router": "^1.170.44",
64
+ "@tanstack/react-query": "^5.104.5",
65
+ "@tanstack/react-router": "^1.170.45",
66
66
  "@types/node": "^26.6.5",
67
67
  "@types/react": "^19.2.2",
68
68
  "@types/react-dom": "^19.2.1",
69
69
  "@vitejs/plugin-react": "^6.0.1",
70
- "i18next": "^26.4.7",
70
+ "i18next": "^26.5.3",
71
71
  "react": "^19.3.1",
72
72
  "react-dom": "^19.3.1",
73
73
  "react-hook-form": "^7.90.0",
74
- "react-i18next": "^17.0.20",
74
+ "react-i18next": "^17.1.0",
75
75
  "tsup": "^9.0.0",
76
76
  "typescript": "~7.0.2",
77
77
  "vite": "^8.3.4",
78
- "@spree/dashboard-core": "1.0.0-rc.5",
79
- "@spree/dashboard-ui": "1.0.0-rc.5",
80
- "@spree/admin-sdk": "1.0.0-rc.5"
78
+ "@spree/admin-sdk": "6.0.0-rc.7",
79
+ "@spree/dashboard-core": "6.0.0-rc.7",
80
+ "@spree/dashboard-ui": "6.0.0-rc.7"
81
81
  },
82
82
  "scripts": {
83
83
  "build": "pnpm generate:routes && tsup",
package/src/api-client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { runtimeListParams, setApiClient } from '@spree/dashboard-core'
1
+ import { i18n, runtimeListParams, setApiClient } from '@spree/dashboard-core'
2
2
  import type { SellerApiClient, SellerExportCreateParams } from '@spree/seller-sdk'
3
3
  import { createSellerClient } from '@spree/seller-sdk'
4
4
 
@@ -146,6 +146,17 @@ export function createSellerApiClient({
146
146
  update: (id, params) => sellerClient().packageTypes.update(id, params),
147
147
  delete: (id) => sellerClient().packageTypes.delete(id),
148
148
  },
149
+ digitalAssets: {
150
+ list: (productId, params) => sellerClient().products.digitalAssets.list(productId, params),
151
+ // Sellers upload files only, so a file is the one thing a new asset must carry.
152
+ create: (productId, { signed_id, ...params }) => {
153
+ if (!signed_id) throw new Error(i18n.t('admin.errors.unexpected'))
154
+ return sellerClient().products.digitalAssets.create(productId, { ...params, signed_id })
155
+ },
156
+ update: (productId, id, params) =>
157
+ sellerClient().products.digitalAssets.update(productId, id, params),
158
+ delete: (productId, id) => sellerClient().products.digitalAssets.delete(productId, id),
159
+ },
149
160
  })
150
161
 
151
162
  return client
@@ -750,7 +750,8 @@
750
750
  "variant_remove_confirm_description": "يُزال من المنتج عند الحفظ.",
751
751
  "variant_remove_confirm_title": "إزالة هذا الخيار؟",
752
752
  "variants": "الخيارات",
753
- "variants_empty": "يُباع هذا المنتج كقطعة واحدة بالسعر أعلاه. أضف خيارًا لبيعه بخيارات مثل المقاس أو اللون."
753
+ "variants_empty": "يُباع هذا المنتج كقطعة واحدة بالسعر أعلاه. أضف خيارًا لبيعه بخيارات مثل المقاس أو اللون.",
754
+ "digital_files_review_notice": "إضافة ملف أو استبداله يعيد هذا المنتج إلى المراجعة، ويتوقف بيعه حتى يعتمده السوق مجددًا."
754
755
  },
755
756
  "profile": {
756
757
  "about": "نبذة",
@@ -738,7 +738,8 @@
738
738
  "variant_remove_confirm_description": "Sie wird beim Speichern vom Produkt entfernt.",
739
739
  "variant_remove_confirm_title": "Diese Variante entfernen?",
740
740
  "variants": "Varianten",
741
- "variants_empty": "Dieses Produkt wird als ein Artikel zum obigen Preis verkauft. Fügen Sie eine Variante hinzu, um es in Optionen wie Größe oder Farbe anzubieten."
741
+ "variants_empty": "Dieses Produkt wird als ein Artikel zum obigen Preis verkauft. Fügen Sie eine Variante hinzu, um es in Optionen wie Größe oder Farbe anzubieten.",
742
+ "digital_files_review_notice": "Das Hinzufügen oder Ersetzen einer Datei schickt dieses Produkt zurück in die Prüfung; es ist nicht verkäuflich, bis der Marktplatz es erneut freigibt."
742
743
  },
743
744
  "profile": {
744
745
  "about": "Über uns",
@@ -738,7 +738,8 @@
738
738
  "variant_remove_confirm_description": "It comes off the product when you save.",
739
739
  "variant_remove_confirm_title": "Remove this variant?",
740
740
  "variants": "Variants",
741
- "variants_empty": "This product is sold as one item, at the price above. Add a variant to sell it in options such as size or colour."
741
+ "variants_empty": "This product is sold as one item, at the price above. Add a variant to sell it in options such as size or colour.",
742
+ "digital_files_review_notice": "Adding or replacing a file sends this product back to review, and it is off sale until the marketplace approves it again."
742
743
  },
743
744
  "profile": {
744
745
  "about": "About",
@@ -738,7 +738,8 @@
738
738
  "variant_remove_confirm_description": "Elle sera retirée du produit à l’enregistrement.",
739
739
  "variant_remove_confirm_title": "Retirer cette variante ?",
740
740
  "variants": "Variantes",
741
- "variants_empty": "Ce produit est vendu comme un article unique, au prix ci-dessus. Ajoutez une variante pour le vendre en plusieurs options, par exemple la taille ou la couleur."
741
+ "variants_empty": "Ce produit est vendu comme un article unique, au prix ci-dessus. Ajoutez une variante pour le vendre en plusieurs options, par exemple la taille ou la couleur.",
742
+ "digital_files_review_notice": "Ajouter ou remplacer un fichier renvoie ce produit en révision ; il n’est plus en vente jusqu’à ce que la place de marché l’approuve de nouveau."
742
743
  },
743
744
  "profile": {
744
745
  "about": "À propos",
@@ -744,7 +744,8 @@
744
744
  "variant_remove_confirm_description": "Zniknie z produktu po zapisaniu.",
745
745
  "variant_remove_confirm_title": "Usunąć ten wariant?",
746
746
  "variants": "Warianty",
747
- "variants_empty": "Ten produkt sprzedajesz jako jedną pozycję, w cenie powyżej. Dodaj wariant, aby sprzedawać go w opcjach takich jak rozmiar czy kolor."
747
+ "variants_empty": "Ten produkt sprzedajesz jako jedną pozycję, w cenie powyżej. Dodaj wariant, aby sprzedawać go w opcjach takich jak rozmiar czy kolor.",
748
+ "digital_files_review_notice": "Dodanie lub zastąpienie pliku wysyła ten produkt ponownie do weryfikacji; do ponownego zatwierdzenia przez marketplace nie jest w sprzedaży."
748
749
  },
749
750
  "profile": {
750
751
  "about": "O sprzedawcy",
@@ -735,7 +735,8 @@
735
735
  "variant_remove_confirm_description": "保存后它会从商品中移除。",
736
736
  "variant_remove_confirm_title": "移除此变体?",
737
737
  "variants": "变体",
738
- "variants_empty": "此商品按上述价格作为单一款式销售。添加变体即可按尺码、颜色等选项销售。"
738
+ "variants_empty": "此商品按上述价格作为单一款式销售。添加变体即可按尺码、颜色等选项销售。",
739
+ "digital_files_review_notice": "添加或替换文件会将此商品重新送审,在平台再次批准前将暂停销售。"
739
740
  },
740
741
  "profile": {
741
742
  "about": "简介",
@@ -1,6 +1,10 @@
1
1
  import {
2
+ AMOUNT_BASED_CALCULATORS,
3
+ CURRENCY_AMOUNT_KEYS,
4
+ CurrencyAmountsField,
2
5
  defaultPreferences,
3
6
  filterPreferenceSchema,
7
+ hasCurrencyAmount,
4
8
  PageHeader,
5
9
  PreferencesForm,
6
10
  type ResourceSearch,
@@ -281,13 +285,15 @@ function DeliveryMethodSheet({
281
285
 
282
286
  form.setValue('calculator', {
283
287
  type: flatRate.type,
284
- preferences: defaultPreferences(renderableSchema(flatRate.schema)),
288
+ preferences: calculatorDefaults(flatRate.type, flatRate.schema),
285
289
  })
286
290
  }, [calculators, methodId, form])
287
291
 
288
- const calculatorSchema = renderableSchema(
292
+ const calculatorSchema = calculatorFormSchema(
293
+ calculatorType,
289
294
  calculators?.data.find((calculator) => calculator.type === calculatorType)?.schema,
290
295
  )
296
+ const amountBased = AMOUNT_BASED_CALCULATORS.includes(calculatorType)
291
297
 
292
298
  const save = useMutation({
293
299
  mutationFn: (values: DeliveryMethodFormValues) => {
@@ -354,7 +360,17 @@ function DeliveryMethodSheet({
354
360
  <SheetDescription>{t('delivery_methods.sheet.description')}</SheetDescription>
355
361
  </SheetHeader>
356
362
  <form
357
- onSubmit={form.handleSubmit((values) => save.mutateAsync(values).catch(() => undefined))}
363
+ onSubmit={form.handleSubmit((values) => {
364
+ // An amount-based calculator with no amount in any currency would never be offered.
365
+ const { type, preferences } = values.calculator
366
+ if (AMOUNT_BASED_CALCULATORS.includes(type) && !hasCurrencyAmount(preferences)) {
367
+ form.setError('calculator.preferences', {
368
+ message: t('admin.currency_amounts.required'),
369
+ })
370
+ return
371
+ }
372
+ return save.mutateAsync(values).catch(() => undefined)
373
+ })}
358
374
  className="flex min-h-0 flex-1 flex-col"
359
375
  >
360
376
  <div className="flex flex-1 flex-col gap-6 overflow-y-auto p-4">
@@ -488,11 +504,10 @@ function DeliveryMethodSheet({
488
504
  value={field.value}
489
505
  onValueChange={(next) => {
490
506
  field.onChange(next)
491
- const schema = renderableSchema(
492
- calculators?.data.find((calculator) => calculator.type === next)
493
- ?.schema,
494
- )
495
- form.setValue('calculator.preferences', defaultPreferences(schema))
507
+ const schema = calculators?.data.find(
508
+ (calculator) => calculator.type === next,
509
+ )?.schema
510
+ form.setValue('calculator.preferences', calculatorDefaults(next, schema))
496
511
  }}
497
512
  >
498
513
  <SelectTrigger id="delivery-method-calculator">
@@ -522,13 +537,24 @@ function DeliveryMethodSheet({
522
537
  <Controller
523
538
  control={form.control}
524
539
  name="calculator.preferences"
525
- render={({ field }) => (
526
- <PreferencesForm
527
- schema={calculatorSchema}
528
- values={field.value}
529
- onChange={field.onChange}
530
- currencyOptions={currencyOptions}
531
- />
540
+ render={({ field, fieldState }) => (
541
+ <>
542
+ {amountBased && sellerProfile && (
543
+ <CurrencyAmountsField
544
+ values={field.value}
545
+ onChange={field.onChange}
546
+ currencies={currencyOptions}
547
+ defaultCurrency={sellerProfile.default_currency}
548
+ error={fieldState.error}
549
+ />
550
+ )}
551
+ <PreferencesForm
552
+ schema={calculatorSchema}
553
+ values={field.value}
554
+ onChange={field.onChange}
555
+ currencyOptions={currencyOptions}
556
+ />
557
+ </>
532
558
  )}
533
559
  />
534
560
  </CardContent>
@@ -634,12 +660,33 @@ function DeliveryMethodSheet({
634
660
  /**
635
661
  * The part of a preference schema the shared form can render.
636
662
  *
637
- * Drops keyed maps — today the flat-rate calculator's
638
- * per-currency `amounts` map, which the operator edits through a dedicated
639
- * multi-currency control and the generic form would show as the hash itself
640
- * in a text box. A seller quotes in the store's currency, so `amount` and
641
- * `currency` are the whole question.
663
+ * Drops keyed maps, which the generic form would show as the hash itself in a
664
+ * text box.
665
+ */
666
+ /**
667
+ * A calculator's settings for the generic form: an amount-based calculator's
668
+ * amounts are edited per currency by the amount editor instead.
642
669
  */
670
+ function calculatorFormSchema(
671
+ type: string,
672
+ schema: PreferenceSchema | undefined,
673
+ ): PreferenceSchema | undefined {
674
+ const renderable = renderableSchema(schema)
675
+ if (!renderable || !AMOUNT_BASED_CALCULATORS.includes(type)) return renderable
676
+
677
+ return filterPreferenceSchema(renderable, (key) => !CURRENCY_AMOUNT_KEYS.includes(key))
678
+ }
679
+
680
+ /**
681
+ * A calculator's starting settings. The single amount's default is 0, which
682
+ * would read as a price someone set, so an amount-based calculator starts with
683
+ * no amount in any currency.
684
+ */
685
+ function calculatorDefaults(type: string, schema: PreferenceSchema | undefined) {
686
+ const defaults = defaultPreferences(calculatorFormSchema(type, schema))
687
+ return AMOUNT_BASED_CALCULATORS.includes(type) ? { ...defaults, amount: null } : defaults
688
+ }
689
+
643
690
  function renderableSchema(schema: PreferenceSchema | undefined): PreferenceSchema | undefined {
644
691
  if (!schema) return undefined
645
692
 
@@ -1,6 +1,7 @@
1
1
  import { zodResolver } from '@hookform/resolvers/zod'
2
2
  import {
3
3
  CategorizationCard,
4
+ DigitalAssetsCard,
4
5
  extensionFormValues,
5
6
  extensionRowValues,
6
7
  extensionSubmitValues,
@@ -16,6 +17,7 @@ import {
16
17
  productFormSchema,
17
18
  productToFormValues,
18
19
  Slot,
20
+ useAssignableVariants,
19
21
  VariantsCard,
20
22
  variantToWirePayload,
21
23
  withExtensionRowValues,
@@ -31,6 +33,7 @@ import { sellerClient } from '../api-client'
31
33
  import { CenteredMessage } from '../components/centered-message'
32
34
  import { ProductStatusCard } from '../components/product-status-card'
33
35
  import { RetryableError } from '../components/retryable-error'
36
+ import { useAccount } from '../hooks/use-account'
34
37
 
35
38
  /** Everything the form edits, in one request. */
36
39
  const PRODUCT_EXPAND = [
@@ -96,6 +99,11 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
96
99
  defaultValues: { ...newProductFormDefaults(), ...extensionFormValues('seller.product', null) },
97
100
  })
98
101
 
102
+ const assignableVariants = useAssignableVariants(form.control, product?.variants)
103
+ // Whether this marketplace lets sellers upload files, and of which types,
104
+ // comes with the account the panel already holds.
105
+ const marketplace = useAccount().data?.store
106
+
99
107
  // Reset once the record arrives, so the inputs show what was last saved
100
108
  // rather than the blank defaults the form mounted with.
101
109
  //
@@ -242,7 +250,7 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
242
250
  <VariantsCard form={form} formKey="seller.product" />
243
251
  <MediaCard
244
252
  productId={productId}
245
- variants={product?.variants ?? []}
253
+ variants={assignableVariants}
246
254
  form={form}
247
255
  formKey="seller.product"
248
256
  />
@@ -253,6 +261,17 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
253
261
  defaultCurrency={profile?.default_currency}
254
262
  />
255
263
  <InventoryCard form={form} />
264
+ <DigitalAssetsCard
265
+ productId={productId}
266
+ variants={assignableVariants}
267
+ readOnly={!marketplace?.seller_digital_assets_enabled}
268
+ accept={marketplace?.seller_digital_asset_content_types}
269
+ notice={
270
+ product?.status === 'active' && marketplace?.review_seller_digital_asset_changes
271
+ ? t('products.digital_files_review_notice')
272
+ : undefined
273
+ }
274
+ />
256
275
  <Slot name="seller.product.form_main" context={{ product, mode }} />
257
276
  </>
258
277
  }