@spree/seller-dashboard 1.0.0-rc.5 → 1.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 +20 -19
- package/src/api-client.ts +12 -1
- package/src/components/orders/fulfillment-edit-dialog.tsx +2 -1
- package/src/components/orders/fulfillment-label-upload-dialog.tsx +1 -1
- package/src/components/orders/fulfillments-card.tsx +3 -2
- package/src/components/orders/order-cards.tsx +1 -1
- package/src/components/orders/post-sale-cards.tsx +3 -3
- package/src/components/orders/returns-card.tsx +2 -1
- package/src/hooks/use-fulfillments.ts +1 -1
- package/src/hooks/use-post-sale.ts +2 -1
- package/src/locales/ar.json +2 -1
- package/src/locales/de.json +2 -1
- package/src/locales/en.json +2 -1
- package/src/locales/fr.json +2 -1
- package/src/locales/pl.json +2 -1
- package/src/locales/zh-CN.json +2 -1
- package/src/pages/delivery-methods.tsx +67 -20
- package/src/pages/product.tsx +33 -5
- package/src/pages/products.tsx +2 -1
- package/src/routes/_authenticated/$sellerId/settings/package-types.tsx +1 -1
- package/src/routes/_authenticated/$sellerId/settings/stock-locations.tsx +1 -1
- package/src/tables/orders.tsx +9 -23
- package/src/tables/payouts.tsx +2 -3
- package/src/tables/products.tsx +1 -3
- package/src/tables/transfers.tsx +3 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/seller-dashboard",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.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,47 +36,48 @@
|
|
|
36
36
|
"LICENSE"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@hookform/resolvers": "^
|
|
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.
|
|
43
|
+
"@tanstack/router-plugin": "^1.168.47",
|
|
44
44
|
"@tanstack/virtual-file-routes": "^1.162.0",
|
|
45
|
-
"lucide-react": "^1.
|
|
45
|
+
"lucide-react": "^1.60.0",
|
|
46
46
|
"zod": "^4.6.7",
|
|
47
|
-
"@spree/
|
|
47
|
+
"@spree/core": "^0.3.0",
|
|
48
|
+
"@spree/seller-sdk": "1.0.0-rc.6"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
50
|
-
"@tanstack/react-query": "^5.104.
|
|
51
|
-
"@tanstack/react-router": "^1.170.
|
|
52
|
-
"i18next": "^26.
|
|
51
|
+
"@tanstack/react-query": "^5.104.5",
|
|
52
|
+
"@tanstack/react-router": "^1.170.45",
|
|
53
|
+
"i18next": "^26.5.3",
|
|
53
54
|
"react": "^19.3.1",
|
|
54
55
|
"react-dom": "^19.3.1",
|
|
55
56
|
"react-hook-form": "^7.90.0",
|
|
56
|
-
"react-i18next": "^17.0
|
|
57
|
-
"@spree/admin-sdk": "^1.0.0-rc.
|
|
58
|
-
"@spree/dashboard-core": "^1.0.0-rc.
|
|
59
|
-
"@spree/dashboard-ui": "^1.0.0-rc.
|
|
57
|
+
"react-i18next": "^17.1.0",
|
|
58
|
+
"@spree/admin-sdk": "^1.0.0-rc.6",
|
|
59
|
+
"@spree/dashboard-core": "^1.0.0-rc.6",
|
|
60
|
+
"@spree/dashboard-ui": "^1.0.0-rc.6"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@biomejs/biome": "^2.4.14",
|
|
63
|
-
"@tanstack/react-query": "^5.104.
|
|
64
|
-
"@tanstack/react-router": "^1.170.
|
|
64
|
+
"@tanstack/react-query": "^5.104.5",
|
|
65
|
+
"@tanstack/react-router": "^1.170.45",
|
|
65
66
|
"@types/node": "^26.6.5",
|
|
66
67
|
"@types/react": "^19.2.2",
|
|
67
68
|
"@types/react-dom": "^19.2.1",
|
|
68
69
|
"@vitejs/plugin-react": "^6.0.1",
|
|
69
|
-
"i18next": "^26.
|
|
70
|
+
"i18next": "^26.5.3",
|
|
70
71
|
"react": "^19.3.1",
|
|
71
72
|
"react-dom": "^19.3.1",
|
|
72
73
|
"react-hook-form": "^7.90.0",
|
|
73
|
-
"react-i18next": "^17.0
|
|
74
|
+
"react-i18next": "^17.1.0",
|
|
74
75
|
"tsup": "^9.0.0",
|
|
75
76
|
"typescript": "~7.0.2",
|
|
76
77
|
"vite": "^8.3.4",
|
|
77
|
-
"@spree/admin-sdk": "1.0.0-rc.
|
|
78
|
-
"@spree/dashboard-core": "1.0.0-rc.
|
|
79
|
-
"@spree/dashboard-ui": "1.0.0-rc.
|
|
78
|
+
"@spree/admin-sdk": "1.0.0-rc.6",
|
|
79
|
+
"@spree/dashboard-core": "1.0.0-rc.6",
|
|
80
|
+
"@spree/dashboard-ui": "1.0.0-rc.6"
|
|
80
81
|
},
|
|
81
82
|
"scripts": {
|
|
82
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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isZeroMoney } from '@spree/core'
|
|
1
2
|
import { formatMoney, useMoneyLocale } from '@spree/dashboard-core'
|
|
2
3
|
import { FulfillmentEditDialog as SharedFulfillmentEditDialog } from '@spree/dashboard-ui'
|
|
3
|
-
import { type Fulfillment,
|
|
4
|
+
import { type Fulfillment, SpreeError } from '@spree/seller-sdk'
|
|
4
5
|
import { useState } from 'react'
|
|
5
6
|
import { useTranslation } from 'react-i18next'
|
|
6
7
|
import { useFulfillmentActions } from '../../hooks/use-fulfillments'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import type { CurrencyCode } from '@spree/core'
|
|
2
3
|
import {
|
|
3
4
|
EMPTY_FILE_UPLOAD_VALUE,
|
|
4
5
|
FileUploadField,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
mapSpreeErrorsToForm,
|
|
10
11
|
} from '@spree/dashboard-core'
|
|
11
12
|
import { LabelUploadDialog } from '@spree/dashboard-ui'
|
|
12
|
-
import type { CurrencyCode } from '@spree/seller-sdk'
|
|
13
13
|
import { useState } from 'react'
|
|
14
14
|
import { useForm } from 'react-hook-form'
|
|
15
15
|
import { useTranslation } from 'react-i18next'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FULFILLMENT_STATUSES, subsetOf } from '@spree/core'
|
|
1
2
|
import { fulfillmentItemRows } from '@spree/dashboard-core'
|
|
2
3
|
import {
|
|
3
4
|
Badge,
|
|
@@ -44,8 +45,8 @@ import { ShippingLabelRow } from './shipping-label-row'
|
|
|
44
45
|
// A parcel that has not gone out is the only one still to ship or cancel.
|
|
45
46
|
// Reading these off "not unfulfilled" once badged a canceled parcel as a
|
|
46
47
|
// success.
|
|
47
|
-
const CAN_SHIP = ['unfulfilled']
|
|
48
|
-
const CAN_CANCEL = ['unfulfilled']
|
|
48
|
+
const CAN_SHIP = subsetOf(FULFILLMENT_STATUSES, ['unfulfilled'])
|
|
49
|
+
const CAN_CANCEL = subsetOf(FULFILLMENT_STATUSES, ['unfulfilled'])
|
|
49
50
|
|
|
50
51
|
/** Every parcel owed on this order, and what the seller can do with each. */
|
|
51
52
|
export function FulfillmentsCard({ order }: { order: Order }) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isZeroMoney } from '@spree/core'
|
|
1
2
|
import {
|
|
2
3
|
AddressFormDialog,
|
|
3
4
|
formatMoney,
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
} from '@spree/dashboard-ui'
|
|
22
23
|
import { EllipsisVerticalIcon, PencilIcon } from '@spree/dashboard-ui/icons'
|
|
23
24
|
import type { Order, OrderAddressParams } from '@spree/seller-sdk'
|
|
24
|
-
import { isZeroMoney } from '@spree/seller-sdk'
|
|
25
25
|
import { type ReactNode, useState } from 'react'
|
|
26
26
|
import { useTranslation } from 'react-i18next'
|
|
27
27
|
import { sellerClient } from '../../api-client'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLAIM_STATUSES, EXCHANGE_STATUSES, subsetOf, sumMoney } from '@spree/core'
|
|
1
2
|
import { currencyParts, formatMoney, isPositiveMoney, useMoneyLocale } from '@spree/dashboard-core'
|
|
2
3
|
import {
|
|
3
4
|
Button,
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
ShieldAlertIcon,
|
|
24
25
|
} from '@spree/dashboard-ui/icons'
|
|
25
26
|
import type { Claim, Order } from '@spree/seller-sdk'
|
|
26
|
-
import { sumMoney } from '@spree/seller-sdk'
|
|
27
27
|
import i18n from 'i18next'
|
|
28
28
|
import { useState } from 'react'
|
|
29
29
|
import { useTranslation } from 'react-i18next'
|
|
@@ -36,8 +36,8 @@ import {
|
|
|
36
36
|
import { lineLabel } from './line-label'
|
|
37
37
|
import { CreateClaimDialog } from './post-sale-create-dialogs'
|
|
38
38
|
|
|
39
|
-
const EXCHANGE_ACTIONABLE = ['requested', 'approved', 'received']
|
|
40
|
-
const CLAIM_ACTIONABLE = ['open', 'approved']
|
|
39
|
+
const EXCHANGE_ACTIONABLE = subsetOf(EXCHANGE_STATUSES, ['requested', 'approved', 'received'])
|
|
40
|
+
const CLAIM_ACTIONABLE = subsetOf(CLAIM_STATUSES, ['open', 'approved'])
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Goods swapped for different ones.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RETURN_STATUSES, subsetOf } from '@spree/core'
|
|
1
2
|
import {
|
|
2
3
|
currencyParts,
|
|
3
4
|
formatMoney,
|
|
@@ -35,7 +36,7 @@ import { CreateReturnDialog, fulfilledUnits } from './post-sale-create-dialogs'
|
|
|
35
36
|
|
|
36
37
|
// Refunded and canceled returns are finished; offering a menu on them would
|
|
37
38
|
// open onto nothing.
|
|
38
|
-
const ACTIONABLE = ['requested', 'approved', 'received']
|
|
39
|
+
const ACTIONABLE = subsetOf(RETURN_STATUSES, ['requested', 'approved', 'received'])
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Goods coming back on this order.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { CurrencyCode } from '@spree/core'
|
|
1
2
|
import { useResourceKeyBuilder } from '@spree/dashboard-core'
|
|
2
3
|
import { toastManager } from '@spree/dashboard-ui'
|
|
3
|
-
import type { CurrencyCode } from '@spree/seller-sdk'
|
|
4
4
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
5
5
|
import { sellerClient } from '../api-client'
|
|
6
6
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClaimResolution } from '@spree/core'
|
|
1
2
|
import { useResourceKey, useResourceKeyBuilder } from '@spree/dashboard-core'
|
|
2
3
|
import { toastManager } from '@spree/dashboard-ui'
|
|
3
4
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
@@ -182,7 +183,7 @@ export function useClaimActions(orderId: string) {
|
|
|
182
183
|
replacementLineItemIds,
|
|
183
184
|
}: {
|
|
184
185
|
claimId: string
|
|
185
|
-
resolution:
|
|
186
|
+
resolution: ClaimResolution
|
|
186
187
|
refundMethod?: 'original_payment' | 'store_credit'
|
|
187
188
|
amount?: string
|
|
188
189
|
replacementLineItemIds?: string[]
|
package/src/locales/ar.json
CHANGED
|
@@ -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": "نبذة",
|
package/src/locales/de.json
CHANGED
|
@@ -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",
|
package/src/locales/en.json
CHANGED
|
@@ -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",
|
package/src/locales/fr.json
CHANGED
|
@@ -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",
|
package/src/locales/pl.json
CHANGED
|
@@ -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",
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -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:
|
|
288
|
+
preferences: calculatorDefaults(flatRate.type, flatRate.schema),
|
|
285
289
|
})
|
|
286
290
|
}, [calculators, methodId, form])
|
|
287
291
|
|
|
288
|
-
const calculatorSchema =
|
|
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) =>
|
|
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 =
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
|
638
|
-
*
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
*
|
|
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
|
|
package/src/pages/product.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
2
|
import {
|
|
3
3
|
CategorizationCard,
|
|
4
|
+
DigitalAssetsCard,
|
|
4
5
|
extensionFormValues,
|
|
6
|
+
extensionRowValues,
|
|
5
7
|
extensionSubmitValues,
|
|
6
8
|
GeneralCard,
|
|
7
9
|
InventoryCard,
|
|
@@ -15,8 +17,10 @@ import {
|
|
|
15
17
|
productFormSchema,
|
|
16
18
|
productToFormValues,
|
|
17
19
|
Slot,
|
|
20
|
+
useAssignableVariants,
|
|
18
21
|
VariantsCard,
|
|
19
22
|
variantToWirePayload,
|
|
23
|
+
withExtensionRowValues,
|
|
20
24
|
} from '@spree/dashboard-core'
|
|
21
25
|
import { FormActions, ResourceLayout, StatusBadge, toastManager } from '@spree/dashboard-ui'
|
|
22
26
|
import type { ProductCreateParams } from '@spree/seller-sdk'
|
|
@@ -29,6 +33,7 @@ import { sellerClient } from '../api-client'
|
|
|
29
33
|
import { CenteredMessage } from '../components/centered-message'
|
|
30
34
|
import { ProductStatusCard } from '../components/product-status-card'
|
|
31
35
|
import { RetryableError } from '../components/retryable-error'
|
|
36
|
+
import { useAccount } from '../hooks/use-account'
|
|
32
37
|
|
|
33
38
|
/** Everything the form edits, in one request. */
|
|
34
39
|
const PRODUCT_EXPAND = [
|
|
@@ -94,6 +99,11 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
94
99
|
defaultValues: { ...newProductFormDefaults(), ...extensionFormValues('seller.product', null) },
|
|
95
100
|
})
|
|
96
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
|
+
|
|
97
107
|
// Reset once the record arrives, so the inputs show what was last saved
|
|
98
108
|
// rather than the blank defaults the form mounted with.
|
|
99
109
|
//
|
|
@@ -106,7 +116,7 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
106
116
|
if (form.formState.isDirty) return
|
|
107
117
|
|
|
108
118
|
form.reset({
|
|
109
|
-
...productToFormValues(product as PanelProduct, product.media),
|
|
119
|
+
...productToFormValues(product as PanelProduct, product.media, 'seller.product'),
|
|
110
120
|
...extensionFormValues('seller.product', product),
|
|
111
121
|
})
|
|
112
122
|
}, [product, form])
|
|
@@ -137,6 +147,7 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
137
147
|
// cards offer both — an allow-list that omitted them let a seller
|
|
138
148
|
// set a crop or paste a video URL and watch it vanish on save.
|
|
139
149
|
media: (values.media ?? []).map((item) => ({
|
|
150
|
+
...extensionRowValues('seller.product.media', item),
|
|
140
151
|
id: item.id,
|
|
141
152
|
signed_id: item.signed_id ?? undefined,
|
|
142
153
|
poster_signed_id: item.poster_signed_id ?? undefined,
|
|
@@ -149,7 +160,7 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
149
160
|
variant_ids: item.variant_ids,
|
|
150
161
|
})),
|
|
151
162
|
variants: (values.variants ?? []).map((variant, index) =>
|
|
152
|
-
variantToWirePayload(variant, index),
|
|
163
|
+
variantToWirePayload(variant, index, 'seller.product'),
|
|
153
164
|
) as ProductCreateParams['variants'],
|
|
154
165
|
}
|
|
155
166
|
|
|
@@ -181,7 +192,7 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
181
192
|
// Raw form state, not `values`: the Zod parse strips keys the schema
|
|
182
193
|
// doesn't know, which is every field a slot widget registered.
|
|
183
194
|
await save.mutateAsync({
|
|
184
|
-
values,
|
|
195
|
+
values: withExtensionRowValues('seller.product', values, form.getValues()),
|
|
185
196
|
extensionValues: extensionSubmitValues('seller.product', form),
|
|
186
197
|
})
|
|
187
198
|
} catch (err) {
|
|
@@ -236,8 +247,13 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
236
247
|
main={
|
|
237
248
|
<>
|
|
238
249
|
<GeneralCard form={form} />
|
|
239
|
-
<VariantsCard form={form} />
|
|
240
|
-
<MediaCard
|
|
250
|
+
<VariantsCard form={form} formKey="seller.product" />
|
|
251
|
+
<MediaCard
|
|
252
|
+
productId={productId}
|
|
253
|
+
variants={assignableVariants}
|
|
254
|
+
form={form}
|
|
255
|
+
formKey="seller.product"
|
|
256
|
+
/>
|
|
241
257
|
<PricesCard
|
|
242
258
|
form={form}
|
|
243
259
|
productName={product?.name ?? ''}
|
|
@@ -245,6 +261,18 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
245
261
|
defaultCurrency={profile?.default_currency}
|
|
246
262
|
/>
|
|
247
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
|
+
/>
|
|
275
|
+
<Slot name="seller.product.form_main" context={{ product, mode }} />
|
|
248
276
|
</>
|
|
249
277
|
}
|
|
250
278
|
sidebar={
|
package/src/pages/products.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PRODUCT_STATUSES, subsetOf } from '@spree/core'
|
|
1
2
|
import type {
|
|
2
3
|
BulkAction,
|
|
3
4
|
BulkActionFormProps,
|
|
@@ -40,7 +41,7 @@ import '../tables/products'
|
|
|
40
41
|
* approves it, so the only moves offered here are the two a seller makes
|
|
41
42
|
* alone (docs/plans/6.0-seller-product-submission.md).
|
|
42
43
|
*/
|
|
43
|
-
const BULK_STATUSES = ['draft', 'archived']
|
|
44
|
+
const BULK_STATUSES = subsetOf(PRODUCT_STATUSES, ['draft', 'archived'])
|
|
44
45
|
|
|
45
46
|
type BulkStatus = (typeof BULK_STATUSES)[number]
|
|
46
47
|
type StatusFormValues = { status: BulkStatus }
|
|
@@ -17,5 +17,5 @@ export const Route = createFileRoute('/_authenticated/$sellerId/settings/package
|
|
|
17
17
|
function PackageTypesRoute() {
|
|
18
18
|
const search = Route.useSearch() as z.infer<typeof packageTypesSearchSchema>
|
|
19
19
|
|
|
20
|
-
return <PackageTypesPage search={search} />
|
|
20
|
+
return <PackageTypesPage search={search} formKey="seller.package_type" />
|
|
21
21
|
}
|
|
@@ -18,5 +18,5 @@ export const Route = createFileRoute('/_authenticated/$sellerId/settings/stock-l
|
|
|
18
18
|
function StockLocationsRoute() {
|
|
19
19
|
const search = Route.useSearch() as z.infer<typeof stockLocationsSearchSchema>
|
|
20
20
|
|
|
21
|
-
return <StockLocationsPage search={search} />
|
|
21
|
+
return <StockLocationsPage search={search} formKey="seller.stock_location" />
|
|
22
22
|
}
|
package/src/tables/orders.tsx
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ORDER_PAYMENT_STATUSES } from '@spree/core'
|
|
2
|
+
import {
|
|
3
|
+
defineTable,
|
|
4
|
+
FILTERABLE_ORDER_FULFILLMENT_STATUSES,
|
|
5
|
+
formatMoney,
|
|
6
|
+
formatStoreDateTime,
|
|
7
|
+
} from '@spree/dashboard-core'
|
|
2
8
|
import { ResourceNameCell, StatusBadge } from '@spree/dashboard-ui'
|
|
3
9
|
import { PackageIcon } from '@spree/dashboard-ui/icons'
|
|
4
10
|
import type { Order } from '@spree/seller-sdk'
|
|
@@ -17,26 +23,6 @@ function OrderDate({ iso }: { iso: string }) {
|
|
|
17
23
|
return <>{formatStoreDateTime(iso, timezone)}</>
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
/** Mirrors the operator dashboard's order status vocabularies. */
|
|
21
|
-
const FULFILLMENT_STATUSES = [
|
|
22
|
-
'unfulfilled',
|
|
23
|
-
'backorder',
|
|
24
|
-
'partial',
|
|
25
|
-
'fulfilled',
|
|
26
|
-
'delivered',
|
|
27
|
-
'canceled',
|
|
28
|
-
] as const
|
|
29
|
-
const PAYMENT_STATUSES = [
|
|
30
|
-
'none',
|
|
31
|
-
'authorized',
|
|
32
|
-
'partially_paid',
|
|
33
|
-
'paid',
|
|
34
|
-
'partially_refunded',
|
|
35
|
-
'refunded',
|
|
36
|
-
'overcharged',
|
|
37
|
-
'voided',
|
|
38
|
-
] as const
|
|
39
|
-
|
|
40
26
|
defineTable<Order>('seller-orders', {
|
|
41
27
|
title: i18n.t('orders.title'),
|
|
42
28
|
searchParam: 'number_cont',
|
|
@@ -69,7 +55,7 @@ defineTable<Order>('seller-orders', {
|
|
|
69
55
|
label: i18n.t('orders.columns.fulfillment'),
|
|
70
56
|
filterable: true,
|
|
71
57
|
filterType: 'enum',
|
|
72
|
-
filterOptions:
|
|
58
|
+
filterOptions: FILTERABLE_ORDER_FULFILLMENT_STATUSES.map((status) => ({
|
|
73
59
|
value: status,
|
|
74
60
|
label: i18n.t(`orders.fulfillment_statuses.${status}`),
|
|
75
61
|
})),
|
|
@@ -90,7 +76,7 @@ defineTable<Order>('seller-orders', {
|
|
|
90
76
|
label: i18n.t('orders.columns.payment'),
|
|
91
77
|
filterable: true,
|
|
92
78
|
filterType: 'enum',
|
|
93
|
-
filterOptions:
|
|
79
|
+
filterOptions: ORDER_PAYMENT_STATUSES.map((status) => ({
|
|
94
80
|
value: status,
|
|
95
81
|
label: i18n.t(`orders.payment_statuses.${status}`),
|
|
96
82
|
})),
|
package/src/tables/payouts.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SELLER_PAYOUT_STATUSES } from '@spree/core'
|
|
1
2
|
import { defineTable, formatMoney, formatStoreDateTime } from '@spree/dashboard-core'
|
|
2
3
|
import { ResourceNameCell, StatusBadge } from '@spree/dashboard-ui'
|
|
3
4
|
import { HandCoinsIcon } from '@spree/dashboard-ui/icons'
|
|
@@ -18,8 +19,6 @@ function LedgerDate({ iso }: { iso: string }) {
|
|
|
18
19
|
return <>{formatStoreDateTime(iso, timezone)}</>
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
const STATUSES = ['pending', 'processing', 'completed', 'failed', 'unresolved'] as const
|
|
22
|
-
|
|
23
22
|
defineTable<Payout>('seller-payouts', {
|
|
24
23
|
title: i18n.t('payouts.title'),
|
|
25
24
|
searchParam: 'search',
|
|
@@ -50,7 +49,7 @@ defineTable<Payout>('seller-payouts', {
|
|
|
50
49
|
sortable: true,
|
|
51
50
|
filterable: true,
|
|
52
51
|
filterType: 'enum',
|
|
53
|
-
filterOptions:
|
|
52
|
+
filterOptions: SELLER_PAYOUT_STATUSES.map((status) => ({
|
|
54
53
|
value: status,
|
|
55
54
|
label: i18n.t(`earnings.statuses.${status}`),
|
|
56
55
|
})),
|
package/src/tables/products.tsx
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
+
import { PRODUCT_STATUSES } from '@spree/core'
|
|
1
2
|
import { defineTable, formatPrice } from '@spree/dashboard-core'
|
|
2
3
|
import { ResourceNameCell, StatusBadge, Thumbnail } from '@spree/dashboard-ui'
|
|
3
4
|
import { PackageIcon } from '@spree/dashboard-ui/icons'
|
|
4
5
|
import type { Product } from '@spree/seller-sdk'
|
|
5
6
|
import i18n from 'i18next'
|
|
6
7
|
|
|
7
|
-
/** Mirrors the operator dashboard's product statuses. */
|
|
8
|
-
const PRODUCT_STATUSES = ['active', 'draft', 'proposed', 'rejected', 'archived'] as const
|
|
9
|
-
|
|
10
8
|
defineTable<Product>('seller-products', {
|
|
11
9
|
title: i18n.t('products.title'),
|
|
12
10
|
searchParam: 'search',
|
package/src/tables/transfers.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SELLER_TRANSFER_KINDS, SELLER_TRANSFER_STATUSES } from '@spree/core'
|
|
1
2
|
import { defineTable, formatMoney, formatStoreDateTime, useTenantId } from '@spree/dashboard-core'
|
|
2
3
|
import { StatusBadge } from '@spree/dashboard-ui'
|
|
3
4
|
import { BanknoteIcon } from '@spree/dashboard-ui/icons'
|
|
@@ -43,9 +44,6 @@ function LedgerDate({ iso }: { iso: string }) {
|
|
|
43
44
|
return <>{formatStoreDateTime(iso, timezone)}</>
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
const STATUSES = ['pending', 'processing', 'completed', 'failed', 'unresolved'] as const
|
|
47
|
-
const KINDS = ['earning', 'refund_reversal'] as const
|
|
48
|
-
|
|
49
47
|
defineTable<Transfer>('seller-transfers', {
|
|
50
48
|
title: i18n.t('earnings.transfers.title'),
|
|
51
49
|
// Without this the toolbar sends `name_cont`, which the ledger does not
|
|
@@ -78,7 +76,7 @@ defineTable<Transfer>('seller-transfers', {
|
|
|
78
76
|
label: i18n.t('earnings.columns.kind'),
|
|
79
77
|
filterable: true,
|
|
80
78
|
filterType: 'enum',
|
|
81
|
-
filterOptions:
|
|
79
|
+
filterOptions: SELLER_TRANSFER_KINDS.map((kind) => ({
|
|
82
80
|
value: kind,
|
|
83
81
|
label: i18n.t(`earnings.kinds.${kind}`),
|
|
84
82
|
})),
|
|
@@ -93,7 +91,7 @@ defineTable<Transfer>('seller-transfers', {
|
|
|
93
91
|
sortable: true,
|
|
94
92
|
filterable: true,
|
|
95
93
|
filterType: 'enum',
|
|
96
|
-
filterOptions:
|
|
94
|
+
filterOptions: SELLER_TRANSFER_STATUSES.map((status) => ({
|
|
97
95
|
value: status,
|
|
98
96
|
label: i18n.t(`earnings.statuses.${status}`),
|
|
99
97
|
})),
|