@spree/dashboard-core 1.0.0-rc.4 → 1.0.0-rc.6
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 +10 -7
- package/src/api-client.ts +76 -7
- package/src/components/address-form-dialog.tsx +2 -2
- package/src/components/country-combobox.tsx +1 -2
- package/src/components/currency-amounts-field.tsx +93 -0
- package/src/components/currency-select.tsx +6 -7
- package/src/components/file-upload-field.tsx +1 -0
- package/src/components/locale-select.tsx +22 -23
- package/src/components/nav-main.tsx +7 -2
- package/src/components/preferences-form.tsx +2 -8
- package/src/components/settings-sidebar.tsx +8 -1
- package/src/components/slot.tsx +21 -1
- package/src/components/table-toolbar.tsx +102 -39
- package/src/hooks/use-display-name.ts +1 -18
- package/src/hooks/use-file-upload.ts +2 -1
- package/src/hooks/use-import.ts +3 -2
- package/src/index.ts +17 -3
- package/src/lib/currencies.ts +2 -34
- package/src/lib/currency-amounts.test.ts +147 -0
- package/src/lib/currency-amounts.ts +172 -0
- package/src/lib/currency-parts.ts +2 -21
- package/src/lib/filters-to-ransack.ts +4 -1
- package/src/lib/form-fields-registry.ts +23 -1
- package/src/lib/form-mappers.ts +1 -1
- package/src/lib/formatters.ts +6 -10
- package/src/lib/fulfillment-items.ts +11 -0
- package/src/lib/i18n.ts +1 -12
- package/src/lib/iso-codes.ts +9 -26
- package/src/lib/money.ts +9 -99
- package/src/lib/normalize-money.ts +2 -85
- package/src/lib/return-refunds.ts +1 -2
- package/src/lib/route-registry.ts +29 -0
- package/src/lib/settings-nav-registry.ts +6 -1
- package/src/lib/store-config.test.ts +93 -0
- package/src/lib/store-config.ts +76 -0
- package/src/lib/table-registry.ts +16 -5
- package/src/locales/ar.json +51 -0
- package/src/locales/de.json +51 -0
- package/src/locales/en.json +51 -0
- package/src/locales/es.json +51 -0
- package/src/locales/fr.json +51 -0
- package/src/locales/pl.json +51 -0
- package/src/locales/zh-CN.json +51 -0
- package/src/pages/package-types.tsx +50 -11
- package/src/pages/settings-index.tsx +4 -1
- package/src/pages/stock-locations.tsx +31 -6
- package/src/products/assignable-variants.ts +35 -0
- package/src/products/bulk-variants-dialog.tsx +27 -28
- package/src/products/digital-asset-edit-sheet.tsx +192 -0
- package/src/products/digital-asset-provider-sheet.tsx +88 -0
- package/src/products/digital-assets-card.tsx +451 -0
- package/src/products/media-edit-sheet.tsx +68 -20
- package/src/products/normalize-quantity.ts +2 -3
- package/src/products/product-bulk-price-editor.tsx +1 -3
- package/src/products/product-form-cards.tsx +67 -37
- package/src/products/product-form-mapping.ts +44 -5
- package/src/products/product-schema.test.ts +8 -0
- package/src/products/product-schema.ts +14 -7
- package/src/products/product-types.ts +1 -1
- package/src/products/provider-settings-fields.tsx +110 -0
- package/src/products/sheet-save-handlers.ts +37 -0
- package/src/products/use-digital-assets.ts +66 -0
- package/src/products/variant-edit-sheet.tsx +21 -11
- package/src/products/variants-section.tsx +4 -1
- package/src/providers/store-provider.tsx +31 -7
- package/src/schemas/package-type.ts +14 -12
- package/src/schemas/stock-location.ts +2 -3
- package/src/tables/stock-locations.tsx +5 -8
- package/src/lib/money.test.ts +0 -63
- package/src/lib/normalize-money.test.ts +0 -67
package/src/locales/pl.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "Nie wybrano adresu"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "Ścieżka nawigacji",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "Domyślny rozliczeniowy",
|
|
99
102
|
"default_shipping": "Domyślny wysyłkowy",
|
|
103
|
+
"inherited_from": "Dziedziczony z: {{name}}",
|
|
100
104
|
"set_default_billing": "Ustaw jako domyślny adres rozliczeniowy",
|
|
101
105
|
"set_default_shipping": "Ustaw jako domyślny adres wysyłkowy"
|
|
102
106
|
},
|
|
@@ -113,6 +117,7 @@
|
|
|
113
117
|
"title": "Zresetuj hasło"
|
|
114
118
|
},
|
|
115
119
|
"login": {
|
|
120
|
+
"account_deactivated": "To konto zostało dezaktywowane. Skontaktuj się z administratorem, aby przywrócić dostęp.",
|
|
116
121
|
"account_not_provisioned": "To konto nie ma dostępu do tego sklepu. Poproś administratora o zaproszenie.",
|
|
117
122
|
"divider": "lub",
|
|
118
123
|
"invalid_oauth_state": "Ta sesja logowania wygasła lub była nieprawidłowa. Zaloguj się ponownie.",
|
|
@@ -484,6 +489,12 @@
|
|
|
484
489
|
"discard_title": "Odrzucić szkic?"
|
|
485
490
|
}
|
|
486
491
|
},
|
|
492
|
+
"currency_amounts": {
|
|
493
|
+
"amount_in_currency": "Kwota w {{currency}}",
|
|
494
|
+
"help": "Waluty bez kwoty nie mają oferowanej tej metody dostawy.",
|
|
495
|
+
"label": "Kwota",
|
|
496
|
+
"required": "Podaj kwotę w co najmniej jednej walucie."
|
|
497
|
+
},
|
|
487
498
|
"custom_field_definitions": {
|
|
488
499
|
"errors": {
|
|
489
500
|
"failed_to_create": "Nie udało się utworzyć definicji pola niestandardowego",
|
|
@@ -938,6 +949,14 @@
|
|
|
938
949
|
"order_multiple": {
|
|
939
950
|
"label": "Wielokrotność zamówienia",
|
|
940
951
|
"help": "Każdy kolejny krok powyżej minimum. Pozostaw puste, aby dopuścić dowolną ilość."
|
|
952
|
+
},
|
|
953
|
+
"store": {
|
|
954
|
+
"digital_asset_authorized_clicks": {
|
|
955
|
+
"label": "Dozwolone pobrania"
|
|
956
|
+
},
|
|
957
|
+
"digital_asset_authorized_days": {
|
|
958
|
+
"label": "Dostępne dni"
|
|
959
|
+
}
|
|
941
960
|
}
|
|
942
961
|
},
|
|
943
962
|
"image_upload": {
|
|
@@ -2063,6 +2082,38 @@
|
|
|
2063
2082
|
"description": "Sprzedawca musi podać adres do zwrotów"
|
|
2064
2083
|
}
|
|
2065
2084
|
}
|
|
2085
|
+
},
|
|
2086
|
+
"digital_assets": {
|
|
2087
|
+
"add": "Dodaj",
|
|
2088
|
+
"all_variants": "Wszystkie warianty",
|
|
2089
|
+
"columns": {
|
|
2090
|
+
"file": "Plik",
|
|
2091
|
+
"variant": "Wariant",
|
|
2092
|
+
"size": "Rozmiar",
|
|
2093
|
+
"downloads": "Pobrania",
|
|
2094
|
+
"days": "Dni"
|
|
2095
|
+
},
|
|
2096
|
+
"delete_description": "„{{name}}” nie będzie już dostępny do pobrania dla osób, które go kupiły. Zmiana działa natychmiast.",
|
|
2097
|
+
"delete_title": "Usunąć ten plik?",
|
|
2098
|
+
"description": "Pliki, które kupujący mogą pobrać po zakupie. Dołącz je do produktu cyfrowego lub do fizycznego, aby dostarczyć instrukcję albo gwarancję.",
|
|
2099
|
+
"download": "Pobierz",
|
|
2100
|
+
"drop_hint": "Przeciągnij i upuść pliki tutaj lub kliknij, aby wybrać",
|
|
2101
|
+
"empty": "Brak plików",
|
|
2102
|
+
"limit_invalid": "Limity muszą być liczbami całkowitymi od 1 lub puste.",
|
|
2103
|
+
"limits_help": "Pozostaw limity pobrań i dni puste, aby użyć ustawień sklepu.",
|
|
2104
|
+
"source": {
|
|
2105
|
+
"upload": "Prześlij plik"
|
|
2106
|
+
},
|
|
2107
|
+
"store_default": "{{count}} (domyślne sklepu)",
|
|
2108
|
+
"title": "Pliki cyfrowe",
|
|
2109
|
+
"untitled": "Plik bez nazwy",
|
|
2110
|
+
"upload": "Dodaj plik",
|
|
2111
|
+
"upload_failed": "Nie udało się przesłać {{name}}: {{message}}",
|
|
2112
|
+
"uploading": "Przesyłanie {{name}}…",
|
|
2113
|
+
"uploading_short": "Przesyłanie…",
|
|
2114
|
+
"replace": "Zastąp plik",
|
|
2115
|
+
"replace_description": "„{{name}}” zostanie usunięty, a wszyscy kupujący pobiorą nowy plik ze swoich dotychczasowych linków. Obowiązuje natychmiast.",
|
|
2116
|
+
"replace_title": "Zastąpić ten plik?"
|
|
2066
2117
|
}
|
|
2067
2118
|
}
|
|
2068
2119
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "未选择地址"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "面包屑导航",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "默认账单",
|
|
99
102
|
"default_shipping": "默认收货",
|
|
103
|
+
"inherited_from": "继承自 {{name}}",
|
|
100
104
|
"set_default_billing": "设为默认账单地址",
|
|
101
105
|
"set_default_shipping": "设为默认收货地址"
|
|
102
106
|
},
|
|
@@ -113,6 +117,7 @@
|
|
|
113
117
|
"title": "重置密码"
|
|
114
118
|
},
|
|
115
119
|
"login": {
|
|
120
|
+
"account_deactivated": "此账户已被停用。请联系管理员恢复访问权限。",
|
|
116
121
|
"account_not_provisioned": "此账户无权访问该店铺。请向管理员申请邀请。",
|
|
117
122
|
"divider": "或",
|
|
118
123
|
"invalid_oauth_state": "登录会话已过期或无效。请重新登录。",
|
|
@@ -483,6 +488,12 @@
|
|
|
483
488
|
"discard_title": "放弃此草稿?"
|
|
484
489
|
}
|
|
485
490
|
},
|
|
491
|
+
"currency_amounts": {
|
|
492
|
+
"amount_in_currency": "{{currency}} 金额",
|
|
493
|
+
"help": "未填写金额的币种将不会提供该配送方式。",
|
|
494
|
+
"label": "金额",
|
|
495
|
+
"required": "请至少为一种货币输入金额。"
|
|
496
|
+
},
|
|
486
497
|
"custom_field_definitions": {
|
|
487
498
|
"errors": {
|
|
488
499
|
"failed_to_create": "创建自定义字段定义失败",
|
|
@@ -937,6 +948,14 @@
|
|
|
937
948
|
"order_multiple": {
|
|
938
949
|
"label": "订购倍数",
|
|
939
950
|
"help": "在最小起订量之上的每一档增量。留空表示不限数量。"
|
|
951
|
+
},
|
|
952
|
+
"store": {
|
|
953
|
+
"digital_asset_authorized_clicks": {
|
|
954
|
+
"label": "允许的下载次数"
|
|
955
|
+
},
|
|
956
|
+
"digital_asset_authorized_days": {
|
|
957
|
+
"label": "可用天数"
|
|
958
|
+
}
|
|
940
959
|
}
|
|
941
960
|
},
|
|
942
961
|
"image_upload": {
|
|
@@ -2033,6 +2052,38 @@
|
|
|
2033
2052
|
"description": "卖家必须提供退货地址"
|
|
2034
2053
|
}
|
|
2035
2054
|
}
|
|
2055
|
+
},
|
|
2056
|
+
"digital_assets": {
|
|
2057
|
+
"add": "添加",
|
|
2058
|
+
"all_variants": "所有变体",
|
|
2059
|
+
"columns": {
|
|
2060
|
+
"file": "文件",
|
|
2061
|
+
"variant": "变体",
|
|
2062
|
+
"size": "大小",
|
|
2063
|
+
"downloads": "下载次数",
|
|
2064
|
+
"days": "天数"
|
|
2065
|
+
},
|
|
2066
|
+
"delete_description": "购买过“{{name}}”的人将无法再下载它。此操作立即生效。",
|
|
2067
|
+
"delete_title": "删除此文件?",
|
|
2068
|
+
"description": "买家购买后可下载的文件。可附加到数字商品,也可附加到实体商品以随货提供说明书或保修卡。",
|
|
2069
|
+
"download": "下载",
|
|
2070
|
+
"drop_hint": "将文件拖放到此处,或点击浏览",
|
|
2071
|
+
"empty": "暂无文件",
|
|
2072
|
+
"limit_invalid": "限制必须是不小于 1 的整数,或留空。",
|
|
2073
|
+
"limits_help": "将下载次数和天数限制留空即可使用商店设置。",
|
|
2074
|
+
"source": {
|
|
2075
|
+
"upload": "上传文件"
|
|
2076
|
+
},
|
|
2077
|
+
"store_default": "{{count}}(商店默认)",
|
|
2078
|
+
"title": "数字文件",
|
|
2079
|
+
"untitled": "未命名文件",
|
|
2080
|
+
"upload": "添加文件",
|
|
2081
|
+
"upload_failed": "上传 {{name}} 失败:{{message}}",
|
|
2082
|
+
"uploading": "正在上传 {{name}}…",
|
|
2083
|
+
"uploading_short": "上传中…",
|
|
2084
|
+
"replace": "替换文件",
|
|
2085
|
+
"replace_description": "“{{name}}”将被移除,所有买家将通过现有链接下载新文件。立即生效。",
|
|
2086
|
+
"replace_title": "替换此文件?"
|
|
2036
2087
|
}
|
|
2037
2088
|
}
|
|
2038
2089
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import { DimensionUnit, WeightUnit } from '@spree/core'
|
|
2
3
|
import {
|
|
3
4
|
Button,
|
|
4
5
|
Field,
|
|
@@ -32,6 +33,7 @@ import { z } from 'zod/v4'
|
|
|
32
33
|
import type { PanelPackageType } from '../api-client'
|
|
33
34
|
import { Can } from '../components/can'
|
|
34
35
|
import { ResourceTable, resourceSearchSchema } from '../components/resource-table'
|
|
36
|
+
import { HostFormSlot } from '../components/slot'
|
|
35
37
|
import {
|
|
36
38
|
canDeletePackageTypes,
|
|
37
39
|
canWritePackageTypes,
|
|
@@ -42,13 +44,12 @@ import {
|
|
|
42
44
|
useUpdatePackageType,
|
|
43
45
|
} from '../hooks/use-package-types'
|
|
44
46
|
import { mapSpreeErrorsToForm } from '../lib/form-errors'
|
|
47
|
+
import { extensionFormValues, extensionSubmitValues } from '../lib/form-fields-registry'
|
|
45
48
|
import { Subject } from '../lib/permissions'
|
|
46
49
|
import { usePermissions } from '../providers/permission-provider'
|
|
47
50
|
import {
|
|
48
|
-
PACKAGE_DIMENSION_UNITS,
|
|
49
51
|
PACKAGE_TYPE_DEFAULTS,
|
|
50
52
|
PACKAGE_TYPE_KINDS,
|
|
51
|
-
PACKAGE_WEIGHT_UNITS,
|
|
52
53
|
type PackageTypeFormValues,
|
|
53
54
|
packageTypeFormSchema,
|
|
54
55
|
packageTypeFormValuesToParams,
|
|
@@ -89,7 +90,14 @@ export type PackageTypesSearch = z.infer<typeof packageTypesSearchSchema>
|
|
|
89
90
|
*/
|
|
90
91
|
function noop() {}
|
|
91
92
|
|
|
92
|
-
export function PackageTypesPage({
|
|
93
|
+
export function PackageTypesPage({
|
|
94
|
+
search,
|
|
95
|
+
formKey = 'package_type',
|
|
96
|
+
}: {
|
|
97
|
+
search: PackageTypesSearch
|
|
98
|
+
/** Names the sheets' slot and extension fields — `seller.package_type` in the seller panel. */
|
|
99
|
+
formKey?: string
|
|
100
|
+
}) {
|
|
93
101
|
const { t } = useTranslation()
|
|
94
102
|
const navigate = useNavigate()
|
|
95
103
|
const confirm = useConfirm()
|
|
@@ -194,16 +202,27 @@ export function PackageTypesPage({ search }: { search: PackageTypesSearch }) {
|
|
|
194
202
|
}
|
|
195
203
|
/>
|
|
196
204
|
|
|
197
|
-
{isCreating &&
|
|
198
|
-
|
|
205
|
+
{isCreating && (
|
|
206
|
+
<CreatePackageTypeSheet formKey={formKey} open onOpenChange={(o) => !o && closeSheet()} />
|
|
207
|
+
)}
|
|
208
|
+
{editId && (
|
|
209
|
+
<EditPackageTypeSheet
|
|
210
|
+
id={editId}
|
|
211
|
+
formKey={formKey}
|
|
212
|
+
open
|
|
213
|
+
onOpenChange={(o) => !o && closeSheet()}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
199
216
|
</>
|
|
200
217
|
)
|
|
201
218
|
}
|
|
202
219
|
|
|
203
220
|
function CreatePackageTypeSheet({
|
|
221
|
+
formKey,
|
|
204
222
|
open,
|
|
205
223
|
onOpenChange,
|
|
206
224
|
}: {
|
|
225
|
+
formKey: string
|
|
207
226
|
open: boolean
|
|
208
227
|
onOpenChange: (open: boolean) => void
|
|
209
228
|
}) {
|
|
@@ -211,12 +230,15 @@ function CreatePackageTypeSheet({
|
|
|
211
230
|
const createMutation = useCreatePackageType()
|
|
212
231
|
const form = useForm<PackageTypeFormValues>({
|
|
213
232
|
resolver: zodResolver(packageTypeFormSchema),
|
|
214
|
-
defaultValues: PACKAGE_TYPE_DEFAULTS,
|
|
233
|
+
defaultValues: { ...PACKAGE_TYPE_DEFAULTS, ...extensionFormValues(formKey, null) },
|
|
215
234
|
})
|
|
216
235
|
|
|
217
236
|
async function onSubmit(values: PackageTypeFormValues) {
|
|
218
237
|
try {
|
|
219
|
-
await createMutation.mutateAsync(
|
|
238
|
+
await createMutation.mutateAsync({
|
|
239
|
+
...packageTypeFormValuesToParams(values),
|
|
240
|
+
...extensionSubmitValues(formKey, form),
|
|
241
|
+
})
|
|
220
242
|
form.reset(PACKAGE_TYPE_DEFAULTS)
|
|
221
243
|
onOpenChange(false)
|
|
222
244
|
} catch (err) {
|
|
@@ -240,6 +262,11 @@ function CreatePackageTypeSheet({
|
|
|
240
262
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
241
263
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
242
264
|
<PackageTypeFormFields form={form} />
|
|
265
|
+
<HostFormSlot
|
|
266
|
+
form={form}
|
|
267
|
+
name={`${formKey}.form_fields`}
|
|
268
|
+
context={{ packageType: undefined, mode: 'new' }}
|
|
269
|
+
/>
|
|
243
270
|
</div>
|
|
244
271
|
<SheetFooter>
|
|
245
272
|
<Button type="submit" disabled={createMutation.isPending}>
|
|
@@ -254,10 +281,12 @@ function CreatePackageTypeSheet({
|
|
|
254
281
|
|
|
255
282
|
function EditPackageTypeSheet({
|
|
256
283
|
id,
|
|
284
|
+
formKey,
|
|
257
285
|
open,
|
|
258
286
|
onOpenChange,
|
|
259
287
|
}: {
|
|
260
288
|
id: string
|
|
289
|
+
formKey: string
|
|
261
290
|
open: boolean
|
|
262
291
|
onOpenChange: (open: boolean) => void
|
|
263
292
|
}) {
|
|
@@ -271,12 +300,17 @@ function EditPackageTypeSheet({
|
|
|
271
300
|
const form = useForm<PackageTypeFormValues>({
|
|
272
301
|
resolver: zodResolver(packageTypeFormSchema),
|
|
273
302
|
defaultValues: PACKAGE_TYPE_DEFAULTS,
|
|
274
|
-
values: packageType
|
|
303
|
+
values: packageType
|
|
304
|
+
? { ...packageTypeToFormValues(packageType), ...extensionFormValues(formKey, packageType) }
|
|
305
|
+
: undefined,
|
|
275
306
|
})
|
|
276
307
|
|
|
277
308
|
async function onSubmit(values: PackageTypeFormValues) {
|
|
278
309
|
try {
|
|
279
|
-
await updateMutation.mutateAsync(
|
|
310
|
+
await updateMutation.mutateAsync({
|
|
311
|
+
...packageTypeFormValuesToParams(values),
|
|
312
|
+
...extensionSubmitValues(formKey, form),
|
|
313
|
+
})
|
|
280
314
|
onOpenChange(false)
|
|
281
315
|
} catch (err) {
|
|
282
316
|
if (!mapSpreeErrorsToForm(err, form.setError)) throw err
|
|
@@ -299,6 +333,11 @@ function EditPackageTypeSheet({
|
|
|
299
333
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
300
334
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
301
335
|
<PackageTypeFormFields form={form} />
|
|
336
|
+
<HostFormSlot
|
|
337
|
+
form={form}
|
|
338
|
+
name={`${formKey}.form_fields`}
|
|
339
|
+
context={{ packageType, mode: 'edit' }}
|
|
340
|
+
/>
|
|
302
341
|
</div>
|
|
303
342
|
<SheetFooter>
|
|
304
343
|
<Button type="submit" disabled={updateMutation.isPending}>
|
|
@@ -376,7 +415,7 @@ function PackageTypeFormFields({ form }: { form: UseFormReturn<PackageTypeFormVa
|
|
|
376
415
|
<UnitField
|
|
377
416
|
form={form}
|
|
378
417
|
name="dimensions_unit"
|
|
379
|
-
units={
|
|
418
|
+
units={Object.values(DimensionUnit)}
|
|
380
419
|
label={t('admin.fields.package_type.dimensions_unit.label')}
|
|
381
420
|
/>
|
|
382
421
|
</div>
|
|
@@ -416,7 +455,7 @@ function PackageTypeFormFields({ form }: { form: UseFormReturn<PackageTypeFormVa
|
|
|
416
455
|
<UnitField
|
|
417
456
|
form={form}
|
|
418
457
|
name="weight_unit"
|
|
419
|
-
units={
|
|
458
|
+
units={Object.values(WeightUnit)}
|
|
420
459
|
label={t('admin.fields.package_type.weight_unit.label')}
|
|
421
460
|
/>
|
|
422
461
|
</div>
|
|
@@ -103,7 +103,10 @@ function SettingsCard({ entry, tenantId }: { entry: SettingsNavEntry; tenantId:
|
|
|
103
103
|
>
|
|
104
104
|
<Icon className="mt-0.5 size-6 shrink-0 text-muted-foreground opacity-80 hover:opacity-100" />
|
|
105
105
|
<span className="flex min-w-0 flex-col gap-1">
|
|
106
|
-
<span className="text-sm font-medium leading-none">
|
|
106
|
+
<span className="flex items-center gap-1.5 text-sm font-medium leading-none">
|
|
107
|
+
{label}
|
|
108
|
+
{entry.badge && <entry.badge />}
|
|
109
|
+
</span>
|
|
107
110
|
{description && <span className="text-sm text-muted-foreground">{description}</span>}
|
|
108
111
|
</span>
|
|
109
112
|
</Link>
|
|
@@ -34,6 +34,7 @@ import { Can } from '../components/can'
|
|
|
34
34
|
import { CountryCombobox } from '../components/country-combobox'
|
|
35
35
|
import { StateCombobox, useCountryStates } from '../components/country-state-fields'
|
|
36
36
|
import { ResourceTable, resourceSearchSchema } from '../components/resource-table'
|
|
37
|
+
import { HostFormSlot } from '../components/slot'
|
|
37
38
|
import {
|
|
38
39
|
canDeleteStockLocations,
|
|
39
40
|
listStockLocations,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
useUpdateStockLocation,
|
|
44
45
|
} from '../hooks/use-stock-locations'
|
|
45
46
|
import { mapSpreeErrorsToForm } from '../lib/form-errors'
|
|
47
|
+
import { extensionFormValues, extensionSubmitValues } from '../lib/form-fields-registry'
|
|
46
48
|
import { Subject } from '../lib/permissions'
|
|
47
49
|
import { usePermissions } from '../providers/permission-provider'
|
|
48
50
|
import {
|
|
@@ -90,8 +92,11 @@ export function StockLocationsPage({
|
|
|
90
92
|
search,
|
|
91
93
|
stockLevelsPanel: StockLevelsPanel,
|
|
92
94
|
activityPanel: ActivityPanel,
|
|
95
|
+
formKey = 'stock_location',
|
|
93
96
|
}: {
|
|
94
97
|
search: StockLocationsSearch
|
|
98
|
+
/** Names the sheets' slot and extension fields — `seller.stock_location` in the seller panel. */
|
|
99
|
+
formKey?: string
|
|
95
100
|
/** Rendered inside the edit sheet, below the form. Optional — see above. */
|
|
96
101
|
stockLevelsPanel?: ComponentType<{ stockLocationId: string }>
|
|
97
102
|
/** What changed in this warehouse, for reconciliation. Optional. */
|
|
@@ -170,10 +175,13 @@ export function StockLocationsPage({
|
|
|
170
175
|
}
|
|
171
176
|
/>
|
|
172
177
|
|
|
173
|
-
{isCreating &&
|
|
178
|
+
{isCreating && (
|
|
179
|
+
<CreateStockLocationSheet formKey={formKey} open onOpenChange={(o) => !o && closeSheet()} />
|
|
180
|
+
)}
|
|
174
181
|
{editId && (
|
|
175
182
|
<EditStockLocationSheet
|
|
176
183
|
id={editId}
|
|
184
|
+
formKey={formKey}
|
|
177
185
|
open
|
|
178
186
|
onOpenChange={(o) => !o && closeSheet()}
|
|
179
187
|
stockLevelsPanel={StockLevelsPanel}
|
|
@@ -189,9 +197,11 @@ export function StockLocationsPage({
|
|
|
189
197
|
// ============================================================================
|
|
190
198
|
|
|
191
199
|
function CreateStockLocationSheet({
|
|
200
|
+
formKey,
|
|
192
201
|
open,
|
|
193
202
|
onOpenChange,
|
|
194
203
|
}: {
|
|
204
|
+
formKey: string
|
|
195
205
|
open: boolean
|
|
196
206
|
onOpenChange: (open: boolean) => void
|
|
197
207
|
}) {
|
|
@@ -201,13 +211,13 @@ function CreateStockLocationSheet({
|
|
|
201
211
|
const form = useForm<StockLocationFormValues>({
|
|
202
212
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
213
|
resolver: zodResolver(stockLocationFormSchema) as any,
|
|
204
|
-
defaultValues: STOCK_LOCATION_DEFAULTS,
|
|
214
|
+
defaultValues: { ...STOCK_LOCATION_DEFAULTS, ...extensionFormValues(formKey, null) },
|
|
205
215
|
})
|
|
206
216
|
|
|
207
217
|
async function onSubmit(values: StockLocationFormValues) {
|
|
208
218
|
try {
|
|
209
219
|
const params = formValuesToParams(values)
|
|
210
|
-
await createMutation.mutateAsync(params)
|
|
220
|
+
await createMutation.mutateAsync({ ...params, ...extensionSubmitValues(formKey, form) })
|
|
211
221
|
form.reset(STOCK_LOCATION_DEFAULTS)
|
|
212
222
|
onOpenChange(false)
|
|
213
223
|
} catch (err) {
|
|
@@ -231,6 +241,11 @@ function CreateStockLocationSheet({
|
|
|
231
241
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
232
242
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
233
243
|
<StockLocationFormFields form={form} />
|
|
244
|
+
<HostFormSlot
|
|
245
|
+
form={form}
|
|
246
|
+
name={`${formKey}.form_fields`}
|
|
247
|
+
context={{ stockLocation: undefined, mode: 'new' }}
|
|
248
|
+
/>
|
|
234
249
|
</div>
|
|
235
250
|
<SheetFooter>
|
|
236
251
|
<Button
|
|
@@ -259,12 +274,14 @@ function CreateStockLocationSheet({
|
|
|
259
274
|
|
|
260
275
|
function EditStockLocationSheet({
|
|
261
276
|
id,
|
|
277
|
+
formKey,
|
|
262
278
|
open,
|
|
263
279
|
onOpenChange,
|
|
264
280
|
stockLevelsPanel: StockLevelsPanel,
|
|
265
281
|
activityPanel: ActivityPanel,
|
|
266
282
|
}: {
|
|
267
283
|
id: string
|
|
284
|
+
formKey: string
|
|
268
285
|
open: boolean
|
|
269
286
|
onOpenChange: (open: boolean) => void
|
|
270
287
|
stockLevelsPanel?: ComponentType<{ stockLocationId: string }>
|
|
@@ -284,14 +301,17 @@ function EditStockLocationSheet({
|
|
|
284
301
|
// with whatever the server last persisted (including external edits).
|
|
285
302
|
useEffect(() => {
|
|
286
303
|
if (stockLocation) {
|
|
287
|
-
form.reset(
|
|
304
|
+
form.reset({
|
|
305
|
+
...stockLocationToFormValues(stockLocation),
|
|
306
|
+
...extensionFormValues(formKey, stockLocation),
|
|
307
|
+
})
|
|
288
308
|
}
|
|
289
|
-
}, [stockLocation, form])
|
|
309
|
+
}, [stockLocation, form, formKey])
|
|
290
310
|
|
|
291
311
|
async function onSubmit(values: StockLocationFormValues) {
|
|
292
312
|
try {
|
|
293
313
|
const params = formValuesToParams(values)
|
|
294
|
-
await updateMutation.mutateAsync(params)
|
|
314
|
+
await updateMutation.mutateAsync({ ...params, ...extensionSubmitValues(formKey, form) })
|
|
295
315
|
form.reset(values)
|
|
296
316
|
onOpenChange(false)
|
|
297
317
|
} catch (err) {
|
|
@@ -314,6 +334,11 @@ function EditStockLocationSheet({
|
|
|
314
334
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
315
335
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
316
336
|
<StockLocationFormFields form={form} />
|
|
337
|
+
<HostFormSlot
|
|
338
|
+
form={form}
|
|
339
|
+
name={`${formKey}.form_fields`}
|
|
340
|
+
context={{ stockLocation, mode: 'edit' }}
|
|
341
|
+
/>
|
|
317
342
|
{StockLevelsPanel && <StockLevelsPanel stockLocationId={id} />}
|
|
318
343
|
{ActivityPanel && <ActivityPanel stockLocationId={id} />}
|
|
319
344
|
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { i18n, type ProductFormValues } from '@spree/dashboard-core'
|
|
2
|
+
import { type Control, useWatch } from 'react-hook-form'
|
|
3
|
+
|
|
4
|
+
interface LabelledVariant {
|
|
5
|
+
id: string
|
|
6
|
+
options_text?: string | null
|
|
7
|
+
sku?: string | null
|
|
8
|
+
name?: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The name a variant goes by in a picker. */
|
|
12
|
+
export function variantLabel(variant: LabelledVariant): string {
|
|
13
|
+
return (
|
|
14
|
+
variant.options_text ||
|
|
15
|
+
variant.name ||
|
|
16
|
+
variant.sku ||
|
|
17
|
+
variant.id ||
|
|
18
|
+
i18n.t('admin.products.variants.variant_label')
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The saved variants a file can be linked to. A card that links files saves
|
|
24
|
+
* at once, so it must not offer a variant the merchant removed in the form but
|
|
25
|
+
* has not saved yet: saving would delete that variant and its files with it.
|
|
26
|
+
* Variants added but not saved have no id to link to.
|
|
27
|
+
*/
|
|
28
|
+
export function useAssignableVariants<V extends { id: string }>(
|
|
29
|
+
control: Control<ProductFormValues>,
|
|
30
|
+
variants: V[] | undefined,
|
|
31
|
+
): V[] {
|
|
32
|
+
const liveVariants = useWatch({ control, name: 'variants' })
|
|
33
|
+
const liveVariantIds = new Set((liveVariants ?? []).map((variant) => variant.id).filter(Boolean))
|
|
34
|
+
return (variants ?? []).filter((variant) => liveVariantIds.has(variant.id))
|
|
35
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DimensionUnit, normalizeMoneyInput, WeightUnit } from '@spree/core'
|
|
1
2
|
import type { ProductFormValues, VariantFormValues } from '@spree/dashboard-core'
|
|
2
3
|
import { useCountries, useCountryDisplayName } from '@spree/dashboard-core'
|
|
3
4
|
import {
|
|
@@ -23,9 +24,6 @@ import {
|
|
|
23
24
|
} from './use-product-form-data'
|
|
24
25
|
import { composeOptionsText } from './variants-matrix'
|
|
25
26
|
|
|
26
|
-
const WEIGHT_UNITS = ['g', 'kg', 'lb', 'oz'] as const
|
|
27
|
-
const DIMENSION_UNITS = ['mm', 'cm', 'in'] as const
|
|
28
|
-
|
|
29
27
|
interface Props {
|
|
30
28
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
29
|
form: UseFormReturn<ProductFormValues, any, any>
|
|
@@ -171,39 +169,24 @@ export function BulkVariantsDialog({ form, open, onOpenChange }: Props) {
|
|
|
171
169
|
set(`variants.${idx}.cartons_per_pallet`, change.value)
|
|
172
170
|
break
|
|
173
171
|
case 'cartonWeight': {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
break
|
|
177
|
-
}
|
|
178
|
-
const parsed = Number(change.value.trim().replace(',', '.'))
|
|
179
|
-
if (!Number.isFinite(parsed) || parsed < 0) break
|
|
180
|
-
set(`variants.${idx}.carton_weight`, parsed)
|
|
172
|
+
const parsed = readMeasure(change.value)
|
|
173
|
+
if (parsed !== undefined) set(`variants.${idx}.carton_weight`, parsed)
|
|
181
174
|
break
|
|
182
175
|
}
|
|
183
176
|
case 'preorderable':
|
|
184
177
|
set(`variants.${idx}.preorderable`, change.value)
|
|
185
178
|
break
|
|
186
179
|
case 'backorderLimit': {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
180
|
+
const parsed = readMeasure(change.value)
|
|
181
|
+
if (parsed !== undefined) {
|
|
182
|
+
set(`variants.${idx}.backorder_limit`, parsed === null ? null : Math.trunc(parsed))
|
|
190
183
|
}
|
|
191
|
-
const parsed = Number(change.value.trim().replace(',', '.'))
|
|
192
|
-
if (!Number.isFinite(parsed) || parsed < 0) break
|
|
193
|
-
set(`variants.${idx}.backorder_limit`, Math.trunc(parsed))
|
|
194
184
|
break
|
|
195
185
|
}
|
|
196
186
|
default: {
|
|
197
|
-
// weight / height / width / depth
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (change.value == null || change.value.trim() === '') {
|
|
201
|
-
set(`variants.${idx}.${change.field}`, null)
|
|
202
|
-
break
|
|
203
|
-
}
|
|
204
|
-
const parsed = Number(change.value.trim().replace(',', '.'))
|
|
205
|
-
if (!Number.isFinite(parsed) || parsed < 0) break
|
|
206
|
-
set(`variants.${idx}.${change.field}`, parsed)
|
|
187
|
+
// weight / height / width / depth
|
|
188
|
+
const parsed = readMeasure(change.value)
|
|
189
|
+
if (parsed !== undefined) set(`variants.${idx}.${change.field}`, parsed)
|
|
207
190
|
break
|
|
208
191
|
}
|
|
209
192
|
}
|
|
@@ -242,8 +225,8 @@ export function BulkVariantsDialog({ form, open, onOpenChange }: Props) {
|
|
|
242
225
|
<BulkVariantsTable
|
|
243
226
|
rows={rows}
|
|
244
227
|
onChange={handleChange}
|
|
245
|
-
weightUnitOptions={unitOptions(
|
|
246
|
-
dimensionUnitOptions={unitOptions(
|
|
228
|
+
weightUnitOptions={unitOptions(Object.values(WeightUnit))}
|
|
229
|
+
dimensionUnitOptions={unitOptions(Object.values(DimensionUnit))}
|
|
247
230
|
taxCategoryOptions={
|
|
248
231
|
taxCategories.length > 0
|
|
249
232
|
? taxCategories.map((c) => ({ value: c.id, label: c.name }))
|
|
@@ -293,3 +276,19 @@ export function BulkVariantsDialog({ form, open, onOpenChange }: Props) {
|
|
|
293
276
|
</Dialog>
|
|
294
277
|
)
|
|
295
278
|
}
|
|
279
|
+
|
|
280
|
+
// Cells show numbers with a period, so they read back the same way, with
|
|
281
|
+
// comma grouping (`1,500`) or a lone decimal comma (`2,5`) also accepted.
|
|
282
|
+
const MEASURE_INPUT = /^(?:(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?|\d+,\d+)$/
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A measurement typed or pasted into a cell: `null` when cleared, `undefined`
|
|
286
|
+
* when it is not a non-negative number, so the cell keeps its value rather
|
|
287
|
+
* than being mangled.
|
|
288
|
+
*/
|
|
289
|
+
function readMeasure(value: string | null | undefined): number | null | undefined {
|
|
290
|
+
const typed = value?.trim() ?? ''
|
|
291
|
+
if (typed === '') return null
|
|
292
|
+
if (!MEASURE_INPUT.test(typed)) return undefined
|
|
293
|
+
return Number(normalizeMoneyInput(typed, 'en'))
|
|
294
|
+
}
|