@base44/app-plugin-commerce 0.7.1 → 0.8.1
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/README.md +4 -10
- package/base44/entities/commerce.ProductReview.jsonc +2 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +7 -3
- package/package.json +1 -1
- package/scripts/install.js +2 -4
- package/skills/commerce/SKILL.md +9 -3
- package/skills/commerce/docs/api-storefront.md +3 -3
- package/skills/commerce/install/01-install.md +8 -24
- package/skills/commerce/install/02-storefront.md +22 -9
- package/skills/commerce/references/admin-localization.md +95 -0
- package/skills/commerce/references/catalog-rendering.md +1 -1
- package/skills/commerce/references/reviews.md +7 -6
- package/src/commerce/admin/README.md +36 -14
- package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
- package/src/commerce/admin/components/AddressForm.jsx +13 -12
- package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
- package/src/commerce/admin/components/CountrySelect.jsx +12 -8
- package/src/commerce/admin/components/DataTable.jsx +3 -2
- package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
- package/src/commerce/admin/components/EmptyState.jsx +2 -1
- package/src/commerce/admin/components/MediaUploader.jsx +3 -2
- package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
- package/src/commerce/admin/components/SearchSelect.jsx +4 -3
- package/src/commerce/admin/context/SettingsContext.jsx +12 -25
- package/src/commerce/admin/hooks/useMoney.js +3 -2
- package/src/commerce/admin/i18n/index.js +54 -0
- package/src/commerce/admin/i18n/locales/de.js +896 -0
- package/src/commerce/admin/i18n/locales/en.js +900 -0
- package/src/commerce/admin/i18n/locales/es.js +896 -0
- package/src/commerce/admin/i18n/locales/fr.js +896 -0
- package/src/commerce/admin/i18n/locales/ja.js +896 -0
- package/src/commerce/admin/i18n/locales/pt.js +896 -0
- package/src/commerce/admin/index.jsx +6 -30
- package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
- package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
- package/src/commerce/admin/layout/Sidebar.jsx +29 -18
- package/src/commerce/admin/layout/Topbar.jsx +4 -3
- package/src/commerce/admin/lib/api.js +3 -2
- package/src/commerce/admin/lib/constants.js +44 -43
- package/src/commerce/admin/lib/format.js +4 -3
- package/src/commerce/admin/lib/paths.js +2 -3
- package/src/commerce/admin/pages/Dashboard.jsx +25 -24
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
- package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
- package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
- package/src/commerce/admin/pages/products/Categories.jsx +14 -13
- package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
- package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
- package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
- package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
- package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
- package/src/commerce/admin/routes.jsx +31 -43
- package/src/commerce/storefront/pickers.jsx +19 -5
- package/src/commerce/storefront/useCheckout.jsx +28 -3
- package/src/commerce/utils/storefront.js +49 -2
|
@@ -16,6 +16,7 @@ import { ChevronDown, ChevronRight, Copy, Trash2, TriangleAlert } from "lucide-r
|
|
|
16
16
|
import { toast } from "sonner";
|
|
17
17
|
|
|
18
18
|
import MoneyInput from "../../../../components/MoneyInput";
|
|
19
|
+
import { t } from "../../../../i18n";
|
|
19
20
|
import MediaUploader from "../../../../components/MediaUploader";
|
|
20
21
|
import ConfirmDialog from "../../../../components/ConfirmDialog";
|
|
21
22
|
import useMoney from "../../../../hooks/useMoney";
|
|
@@ -36,11 +37,11 @@ const fromDateInput = (v) => (v ? new Date(`${v}T00:00:00Z`).toISOString() : nul
|
|
|
36
37
|
* and the attribute combination are what makes a variant itself, so they stay.
|
|
37
38
|
*/
|
|
38
39
|
const COPY_GROUPS = [
|
|
39
|
-
{ key: "price", label: "
|
|
40
|
-
{ key: "inventory", label: "
|
|
40
|
+
{ key: "price", label: t("products.section_price"), fields: ["regular_price", "sale_price", "date_on_sale_from", "date_on_sale_to"] },
|
|
41
|
+
{ key: "inventory", label: t("products.section_inventory"), fields: ["manage_stock", "stock_quantity", "backorders", "stock_status"] },
|
|
41
42
|
{
|
|
42
43
|
key: "shipping",
|
|
43
|
-
label: "
|
|
44
|
+
label: t("products.section_shipping"),
|
|
44
45
|
fields: ["weight", "dimensions", "virtual", "downloadable"],
|
|
45
46
|
},
|
|
46
47
|
];
|
|
@@ -77,12 +78,12 @@ function PriceFields({ values, set, hideRegular = false }) {
|
|
|
77
78
|
<>
|
|
78
79
|
{!hideRegular && (
|
|
79
80
|
<div className="space-y-1.5">
|
|
80
|
-
<Label className="text-xs">
|
|
81
|
+
<Label className="text-xs">{t("products.regular_price")}</Label>
|
|
81
82
|
<MoneyInput value={values.regular_price} onChange={(v) => set({ regular_price: v })} />
|
|
82
83
|
</div>
|
|
83
84
|
)}
|
|
84
85
|
<div className="space-y-1.5">
|
|
85
|
-
<Label className="text-xs">
|
|
86
|
+
<Label className="text-xs">{t("products.sale_price")}</Label>
|
|
86
87
|
<MoneyInput value={values.sale_price} onChange={(v) => set({ sale_price: v })} />
|
|
87
88
|
</div>
|
|
88
89
|
<div className="sm:col-span-2">
|
|
@@ -97,13 +98,13 @@ function PriceFields({ values, set, hideRegular = false }) {
|
|
|
97
98
|
)
|
|
98
99
|
}
|
|
99
100
|
/>
|
|
100
|
-
|
|
101
|
+
{t("products.schedule_sale")}
|
|
101
102
|
</label>
|
|
102
103
|
</div>
|
|
103
104
|
{scheduled && (
|
|
104
105
|
<>
|
|
105
106
|
<div className="space-y-1.5">
|
|
106
|
-
<Label className="text-xs">
|
|
107
|
+
<Label className="text-xs">{t("products.sale_start")}</Label>
|
|
107
108
|
<Input
|
|
108
109
|
type="date"
|
|
109
110
|
value={toDateInput(values.date_on_sale_from)}
|
|
@@ -111,7 +112,7 @@ function PriceFields({ values, set, hideRegular = false }) {
|
|
|
111
112
|
/>
|
|
112
113
|
</div>
|
|
113
114
|
<div className="space-y-1.5">
|
|
114
|
-
<Label className="text-xs">
|
|
115
|
+
<Label className="text-xs">{t("products.sale_end")}</Label>
|
|
115
116
|
<Input
|
|
116
117
|
type="date"
|
|
117
118
|
value={toDateInput(values.date_on_sale_to)}
|
|
@@ -140,7 +141,7 @@ function ShippingFields({ values, set, units }) {
|
|
|
140
141
|
return (
|
|
141
142
|
<>
|
|
142
143
|
<div className="space-y-1.5">
|
|
143
|
-
<Label className="text-xs">
|
|
144
|
+
<Label className="text-xs">{t("products.weight_with_unit", { unit: units.weightUnit })}</Label>
|
|
144
145
|
<Input
|
|
145
146
|
type="number"
|
|
146
147
|
step="any"
|
|
@@ -149,7 +150,7 @@ function ShippingFields({ values, set, units }) {
|
|
|
149
150
|
/>
|
|
150
151
|
</div>
|
|
151
152
|
<div className="space-y-1.5">
|
|
152
|
-
<Label className="text-xs">
|
|
153
|
+
<Label className="text-xs">{t("products.dimensions_with_unit", { unit: units.dimensionUnit })}</Label>
|
|
153
154
|
<div className="grid grid-cols-3 gap-1">
|
|
154
155
|
{["length", "width", "height"].map((k) => (
|
|
155
156
|
<Input
|
|
@@ -176,7 +177,7 @@ function DownloadsFields({ values, set }) {
|
|
|
176
177
|
<div key={i} className="flex items-center gap-2">
|
|
177
178
|
<Input
|
|
178
179
|
className="w-48"
|
|
179
|
-
placeholder="
|
|
180
|
+
placeholder={t("common.name")}
|
|
180
181
|
value={d.name || ""}
|
|
181
182
|
onChange={(e) =>
|
|
182
183
|
set({ downloads: downloads.map((x, j) => (j === i ? { ...x, name: e.target.value } : x)) })
|
|
@@ -184,7 +185,7 @@ function DownloadsFields({ values, set }) {
|
|
|
184
185
|
/>
|
|
185
186
|
<Input
|
|
186
187
|
className="flex-1"
|
|
187
|
-
placeholder="
|
|
188
|
+
placeholder={t("products.file_url")}
|
|
188
189
|
value={d.file_url || ""}
|
|
189
190
|
onChange={(e) =>
|
|
190
191
|
set({ downloads: downloads.map((x, j) => (j === i ? { ...x, file_url: e.target.value } : x)) })
|
|
@@ -219,15 +220,15 @@ function DownloadsFields({ values, set }) {
|
|
|
219
220
|
function SingleProductFields({ product, up, units }) {
|
|
220
221
|
return (
|
|
221
222
|
<div className="grid max-w-2xl gap-4 sm:grid-cols-2">
|
|
222
|
-
<Section label="
|
|
223
|
+
<Section label={t("products.section_price")}>
|
|
223
224
|
<PriceFields values={product} set={up} />
|
|
224
225
|
</Section>
|
|
225
226
|
|
|
226
|
-
<Section label="
|
|
227
|
+
<Section label={t("products.section_inventory")}>
|
|
227
228
|
<div className="space-y-1.5">
|
|
228
|
-
<Label className="text-xs">
|
|
229
|
+
<Label className="text-xs">{t("products.sku")}</Label>
|
|
229
230
|
<Input
|
|
230
|
-
placeholder="
|
|
231
|
+
placeholder={t("products.sku_placeholder")}
|
|
231
232
|
value={product.sku || ""}
|
|
232
233
|
onChange={(e) => up({ sku: e.target.value })}
|
|
233
234
|
/>
|
|
@@ -239,13 +240,13 @@ function SingleProductFields({ product, up, units }) {
|
|
|
239
240
|
checked={product.manage_stock}
|
|
240
241
|
onCheckedChange={(v) => up({ manage_stock: Boolean(v) })}
|
|
241
242
|
/>
|
|
242
|
-
|
|
243
|
+
{t("products.track_stock_quantity")}
|
|
243
244
|
</label>
|
|
244
245
|
</div>
|
|
245
246
|
{product.manage_stock ? (
|
|
246
247
|
<>
|
|
247
248
|
<div className="space-y-1.5">
|
|
248
|
-
<Label className="text-xs">
|
|
249
|
+
<Label className="text-xs">{t("products.stock_quantity")}</Label>
|
|
249
250
|
<Input
|
|
250
251
|
type="number"
|
|
251
252
|
value={product.stock_quantity ?? ""}
|
|
@@ -253,7 +254,7 @@ function SingleProductFields({ product, up, units }) {
|
|
|
253
254
|
/>
|
|
254
255
|
</div>
|
|
255
256
|
<div className="space-y-1.5">
|
|
256
|
-
<Label className="text-xs">
|
|
257
|
+
<Label className="text-xs">{t("products.backorders")}</Label>
|
|
257
258
|
<Select value={product.backorders || "no"} onValueChange={(v) => up({ backorders: v })}>
|
|
258
259
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
259
260
|
<SelectContent>
|
|
@@ -264,10 +265,10 @@ function SingleProductFields({ product, up, units }) {
|
|
|
264
265
|
</Select>
|
|
265
266
|
</div>
|
|
266
267
|
<div className="space-y-1.5">
|
|
267
|
-
<Label className="text-xs">
|
|
268
|
+
<Label className="text-xs">{t("products.low_stock_threshold")}</Label>
|
|
268
269
|
<Input
|
|
269
270
|
type="number"
|
|
270
|
-
placeholder="
|
|
271
|
+
placeholder={t("products.store_default")}
|
|
271
272
|
value={product.low_stock_amount ?? ""}
|
|
272
273
|
onChange={(e) => up({ low_stock_amount: num(e.target.value) })}
|
|
273
274
|
/>
|
|
@@ -275,7 +276,7 @@ function SingleProductFields({ product, up, units }) {
|
|
|
275
276
|
</>
|
|
276
277
|
) : (
|
|
277
278
|
<div className="space-y-1.5">
|
|
278
|
-
<Label className="text-xs">
|
|
279
|
+
<Label className="text-xs">{t("products.stock_status")}</Label>
|
|
279
280
|
<Select value={product.stock_status || "instock"} onValueChange={(v) => up({ stock_status: v })}>
|
|
280
281
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
281
282
|
<SelectContent>
|
|
@@ -289,7 +290,7 @@ function SingleProductFields({ product, up, units }) {
|
|
|
289
290
|
</Section>
|
|
290
291
|
|
|
291
292
|
{!product.virtual && (
|
|
292
|
-
<Section label="
|
|
293
|
+
<Section label={t("products.section_shipping")}>
|
|
293
294
|
<ShippingFields values={product} set={up} units={units} />
|
|
294
295
|
</Section>
|
|
295
296
|
)}
|
|
@@ -301,28 +302,28 @@ function SingleProductFields({ product, up, units }) {
|
|
|
301
302
|
function VariantDetails({ variation, set, units, virtual }) {
|
|
302
303
|
return (
|
|
303
304
|
<div className="grid gap-4 sm:grid-cols-[140px_1fr]">
|
|
304
|
-
<MediaUploader value={variation.image || null} onChange={(image) => set({ image })} label="
|
|
305
|
+
<MediaUploader value={variation.image || null} onChange={(image) => set({ image })} label={t("products.image")} />
|
|
305
306
|
|
|
306
307
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
307
|
-
<Section label="
|
|
308
|
+
<Section label={t("products.section_sale")}>
|
|
308
309
|
<PriceFields values={variation} set={set} hideRegular />
|
|
309
310
|
</Section>
|
|
310
311
|
|
|
311
|
-
<Section label="
|
|
312
|
+
<Section label={t("products.section_inventory")}>
|
|
312
313
|
<div className="space-y-1.5">
|
|
313
|
-
<Label className="text-xs">
|
|
314
|
+
<Label className="text-xs">{t("products.stock_management")}</Label>
|
|
314
315
|
<Select value={variation.manage_stock || "parent"} onValueChange={(v) => set({ manage_stock: v })}>
|
|
315
316
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
316
317
|
<SelectContent>
|
|
317
|
-
<SelectItem value="parent">
|
|
318
|
-
<SelectItem value="yes">
|
|
319
|
-
<SelectItem value="no">
|
|
318
|
+
<SelectItem value="parent">{t("products.stock_same_as_product")}</SelectItem>
|
|
319
|
+
<SelectItem value="yes">{t("products.stock_track_quantity")}</SelectItem>
|
|
320
|
+
<SelectItem value="no">{t("products.stock_dont_track")}</SelectItem>
|
|
320
321
|
</SelectContent>
|
|
321
322
|
</Select>
|
|
322
323
|
</div>
|
|
323
324
|
{variation.manage_stock === "yes" && (
|
|
324
325
|
<div className="space-y-1.5">
|
|
325
|
-
<Label className="text-xs">
|
|
326
|
+
<Label className="text-xs">{t("products.backorders")}</Label>
|
|
326
327
|
<Select value={variation.backorders || "no"} onValueChange={(v) => set({ backorders: v })}>
|
|
327
328
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
328
329
|
<SelectContent>
|
|
@@ -336,29 +337,29 @@ function VariantDetails({ variation, set, units, virtual }) {
|
|
|
336
337
|
</Section>
|
|
337
338
|
|
|
338
339
|
{!virtual && (
|
|
339
|
-
<Section label="
|
|
340
|
+
<Section label={t("products.section_shipping")}>
|
|
340
341
|
<ShippingFields values={variation} set={set} units={units} />
|
|
341
342
|
</Section>
|
|
342
343
|
)}
|
|
343
344
|
|
|
344
|
-
<Section label="
|
|
345
|
+
<Section label={t("products.section_type")}>
|
|
345
346
|
<div className="flex items-end gap-4 text-sm">
|
|
346
347
|
<label className="flex items-center gap-1.5">
|
|
347
348
|
<Checkbox checked={variation.virtual} onCheckedChange={(v) => set({ virtual: Boolean(v) })} />
|
|
348
|
-
|
|
349
|
+
{t("products.virtual")}
|
|
349
350
|
</label>
|
|
350
351
|
<label className="flex items-center gap-1.5">
|
|
351
352
|
<Checkbox
|
|
352
353
|
checked={variation.downloadable}
|
|
353
354
|
onCheckedChange={(v) => set({ downloadable: Boolean(v) })}
|
|
354
355
|
/>
|
|
355
|
-
|
|
356
|
+
{t("products.downloadable")}
|
|
356
357
|
</label>
|
|
357
358
|
</div>
|
|
358
359
|
</Section>
|
|
359
360
|
|
|
360
361
|
{variation.downloadable && (
|
|
361
|
-
<Section label="
|
|
362
|
+
<Section label={t("products.section_files")}>
|
|
362
363
|
<DownloadsFields values={variation} set={set} />
|
|
363
364
|
</Section>
|
|
364
365
|
)}
|
|
@@ -379,18 +380,18 @@ function TaxRules({ product, up, taxGroups }) {
|
|
|
379
380
|
onClick={() => setOpen((o) => !o)}
|
|
380
381
|
>
|
|
381
382
|
{open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
382
|
-
|
|
383
|
+
{t("products.tax_purchase_rules")}
|
|
383
384
|
{!open && (
|
|
384
385
|
<span className="font-normal text-xs">
|
|
385
386
|
· {taxLabel} · {product.tax_group || "Products"}
|
|
386
|
-
{product.sold_individually ?
|
|
387
|
+
{product.sold_individually ? ` · ${t("products.max_one_per_order")}` : ""}
|
|
387
388
|
</span>
|
|
388
389
|
)}
|
|
389
390
|
</button>
|
|
390
391
|
{open && (
|
|
391
392
|
<div className="mt-3 grid max-w-md gap-3 sm:grid-cols-2">
|
|
392
393
|
<div className="space-y-1.5">
|
|
393
|
-
<Label>
|
|
394
|
+
<Label>{t("products.tax_status")}</Label>
|
|
394
395
|
<Select value={product.tax_status} onValueChange={(v) => up({ tax_status: v })}>
|
|
395
396
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
396
397
|
<SelectContent>
|
|
@@ -401,7 +402,7 @@ function TaxRules({ product, up, taxGroups }) {
|
|
|
401
402
|
</Select>
|
|
402
403
|
</div>
|
|
403
404
|
<div className="space-y-1.5">
|
|
404
|
-
<Label>
|
|
405
|
+
<Label>{t("products.tax_group")}</Label>
|
|
405
406
|
<Select value={product.tax_group || "Products"} onValueChange={(v) => up({ tax_group: v })}>
|
|
406
407
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
407
408
|
<SelectContent>
|
|
@@ -416,7 +417,7 @@ function TaxRules({ product, up, taxGroups }) {
|
|
|
416
417
|
checked={product.sold_individually}
|
|
417
418
|
onCheckedChange={(v) => up({ sold_individually: Boolean(v) })}
|
|
418
419
|
/>
|
|
419
|
-
|
|
420
|
+
{t("products.sold_individually")}
|
|
420
421
|
</label>
|
|
421
422
|
</div>
|
|
422
423
|
)}
|
|
@@ -555,7 +556,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
555
556
|
|
|
556
557
|
const applyBulkPrice = () => {
|
|
557
558
|
setVariations(variations.map((v, i) => (selected.includes(i) ? { ...v, regular_price: bulkPrice } : v)));
|
|
558
|
-
toast.success(
|
|
559
|
+
toast.success(t("products.bulk_price_set", { count: selected.length }));
|
|
559
560
|
};
|
|
560
561
|
const applyBulkStock = () => {
|
|
561
562
|
const qty = Number(bulkStock);
|
|
@@ -564,7 +565,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
564
565
|
selected.includes(i) ? { ...v, manage_stock: "yes", stock_quantity: qty } : v
|
|
565
566
|
)
|
|
566
567
|
);
|
|
567
|
-
toast.success(
|
|
568
|
+
toast.success(t("products.bulk_stock_set", { count: selected.length }));
|
|
568
569
|
};
|
|
569
570
|
/** Push one variant's price, inventory and shipping onto the selected rows. */
|
|
570
571
|
const applyCopy = () => {
|
|
@@ -579,7 +580,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
579
580
|
return { ...v, ...patch };
|
|
580
581
|
})
|
|
581
582
|
);
|
|
582
|
-
toast.success(
|
|
583
|
+
toast.success(t("products.bulk_copied", { count: selected.filter((i) => i !== Number(copyFrom)).length }));
|
|
583
584
|
};
|
|
584
585
|
|
|
585
586
|
const switchToSingle = () => {
|
|
@@ -601,16 +602,16 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
601
602
|
<div>
|
|
602
603
|
<div className="inline-flex rounded-lg border bg-muted/60 p-0.5">
|
|
603
604
|
<button type="button" className={modeBtn(mode === "single")} onClick={switchToSingle}>
|
|
604
|
-
|
|
605
|
+
{t("products.single_product")}
|
|
605
606
|
</button>
|
|
606
607
|
<button type="button" className={modeBtn(mode === "variants")} onClick={() => setMode("variants")}>
|
|
607
|
-
|
|
608
|
+
{t("products.has_variants")}
|
|
608
609
|
</button>
|
|
609
610
|
</div>
|
|
610
611
|
<p className="mt-1.5 text-xs text-muted-foreground">
|
|
611
612
|
{mode === "single"
|
|
612
|
-
? "
|
|
613
|
-
: "
|
|
613
|
+
? t("products.single_hint")
|
|
614
|
+
: t("products.variants_hint")}
|
|
614
615
|
</p>
|
|
615
616
|
</div>
|
|
616
617
|
|
|
@@ -624,33 +625,33 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
624
625
|
<div className="space-y-2 border-t pt-4">
|
|
625
626
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
626
627
|
<span className="text-sm font-semibold">
|
|
627
|
-
{
|
|
628
|
+
{t("products.variant_count", { count: variations.length })}
|
|
628
629
|
</span>
|
|
629
630
|
<span className="text-xs text-muted-foreground">
|
|
630
|
-
|
|
631
|
+
{t("products.store_price")}{" "}
|
|
631
632
|
<span className="font-semibold text-foreground">
|
|
632
|
-
{fromPrice != null ?
|
|
633
|
+
{fromPrice != null ? t("products.from_price", { price: money.format(fromPrice) }) : "—"}
|
|
633
634
|
</span>{" "}
|
|
634
|
-
(
|
|
635
|
+
{t("products.lowest_enabled_variant")}
|
|
635
636
|
</span>
|
|
636
637
|
</div>
|
|
637
638
|
|
|
638
639
|
{selected.length > 0 && (
|
|
639
640
|
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 p-2 text-sm">
|
|
640
|
-
<span className="font-medium">{selected.length}
|
|
641
|
+
<span className="font-medium">{t("common.selected_count", { count: selected.length })}</span>
|
|
641
642
|
<MoneyInput className="w-32" value={bulkPrice} onChange={setBulkPrice} />
|
|
642
643
|
<Button type="button" variant="outline" size="sm" disabled={bulkPrice == null} onClick={applyBulkPrice}>
|
|
643
|
-
|
|
644
|
+
{t("products.set_price")}
|
|
644
645
|
</Button>
|
|
645
646
|
<Input
|
|
646
647
|
type="number"
|
|
647
648
|
className="h-9 w-24"
|
|
648
|
-
placeholder="
|
|
649
|
+
placeholder={t("products.qty")}
|
|
649
650
|
value={bulkStock}
|
|
650
651
|
onChange={(e) => setBulkStock(e.target.value)}
|
|
651
652
|
/>
|
|
652
653
|
<Button type="button" variant="outline" size="sm" disabled={bulkStock === ""} onClick={applyBulkStock}>
|
|
653
|
-
|
|
654
|
+
{t("products.set_stock")}
|
|
654
655
|
</Button>
|
|
655
656
|
<div className="flex items-center gap-1.5">
|
|
656
657
|
<Copy className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
@@ -665,11 +666,11 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
665
666
|
</SelectContent>
|
|
666
667
|
</Select>
|
|
667
668
|
<Button type="button" variant="outline" size="sm" onClick={applyCopy}>
|
|
668
|
-
|
|
669
|
+
{t("products.copy_to_selected")}
|
|
669
670
|
</Button>
|
|
670
671
|
</div>
|
|
671
672
|
<Button type="button" variant="ghost" size="sm" className="ml-auto" onClick={() => setSelected([])}>
|
|
672
|
-
|
|
673
|
+
{t("products.clear")}
|
|
673
674
|
</Button>
|
|
674
675
|
</div>
|
|
675
676
|
)}
|
|
@@ -677,11 +678,8 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
677
678
|
{variations.length === 0 ? (
|
|
678
679
|
<Alert>
|
|
679
680
|
<TriangleAlert className="h-4 w-4" />
|
|
680
|
-
<AlertTitle>
|
|
681
|
-
<AlertDescription>
|
|
682
|
-
Pick the values this product comes in for the option above and its variants
|
|
683
|
-
appear here automatically.
|
|
684
|
-
</AlertDescription>
|
|
681
|
+
<AlertTitle>{t("products.no_variants_title")}</AlertTitle>
|
|
682
|
+
<AlertDescription>{t("products.no_variants_description")}</AlertDescription>
|
|
685
683
|
</Alert>
|
|
686
684
|
) : (
|
|
687
685
|
<div className="overflow-x-auto rounded-md border">
|
|
@@ -692,15 +690,15 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
692
690
|
<Checkbox
|
|
693
691
|
checked={allSelected}
|
|
694
692
|
onCheckedChange={(v) => setSelected(v ? variations.map((_, i) => i) : [])}
|
|
695
|
-
aria-label="
|
|
693
|
+
aria-label={t("products.select_all_variants")}
|
|
696
694
|
/>
|
|
697
695
|
</th>
|
|
698
|
-
<th className="p-2 font-semibold">
|
|
699
|
-
<th className="w-36 p-2 font-semibold">
|
|
700
|
-
<th className="w-24 p-2 font-semibold">
|
|
701
|
-
<th className="w-48 p-2 font-semibold">
|
|
702
|
-
<th className="w-16 p-2 text-center font-semibold">
|
|
703
|
-
<th className="w-20 p-2 text-center font-semibold">
|
|
696
|
+
<th className="p-2 font-semibold">{t("products.variant")}</th>
|
|
697
|
+
<th className="w-36 p-2 font-semibold">{t("products.price")}</th>
|
|
698
|
+
<th className="w-24 p-2 font-semibold">{t("products.stock")}</th>
|
|
699
|
+
<th className="w-48 p-2 font-semibold">{t("products.sku")}</th>
|
|
700
|
+
<th className="w-16 p-2 text-center font-semibold">{t("products.default")}</th>
|
|
701
|
+
<th className="w-20 p-2 text-center font-semibold">{t("products.enabled")}</th>
|
|
704
702
|
</tr>
|
|
705
703
|
</thead>
|
|
706
704
|
<tbody>
|
|
@@ -711,7 +709,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
711
709
|
<Checkbox
|
|
712
710
|
checked={selected.includes(i)}
|
|
713
711
|
onCheckedChange={(on) => toggleSelect(i, Boolean(on))}
|
|
714
|
-
aria-label={
|
|
712
|
+
aria-label={t("products.select_variant", { name: variationLabel(v) })}
|
|
715
713
|
/>
|
|
716
714
|
</td>
|
|
717
715
|
<td className="p-2">
|
|
@@ -719,7 +717,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
719
717
|
type="button"
|
|
720
718
|
className="flex items-center gap-1.5 text-left font-medium"
|
|
721
719
|
onClick={() => setExpanded(expanded === i ? null : i)}
|
|
722
|
-
title="
|
|
720
|
+
title={t("products.more_fields")}
|
|
723
721
|
>
|
|
724
722
|
{expanded === i ? (
|
|
725
723
|
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
@@ -739,7 +737,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
739
737
|
<Input
|
|
740
738
|
type="number"
|
|
741
739
|
placeholder="—"
|
|
742
|
-
title={v.manage_stock === "yes" ? "
|
|
740
|
+
title={v.manage_stock === "yes" ? t("products.tracked_quantity") : t("products.not_tracked")}
|
|
743
741
|
value={v.manage_stock === "yes" ? v.stock_quantity ?? "" : ""}
|
|
744
742
|
onChange={(e) =>
|
|
745
743
|
setVariation(
|
|
@@ -764,7 +762,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
764
762
|
className="h-4 w-4 accent-foreground"
|
|
765
763
|
checked={isDefault(v)}
|
|
766
764
|
onChange={() => makeDefault(v)}
|
|
767
|
-
aria-label={
|
|
765
|
+
aria-label={t("products.make_default", { name: variationLabel(v) })}
|
|
768
766
|
/>
|
|
769
767
|
</td>
|
|
770
768
|
<td className="p-2 text-center">
|
|
@@ -773,7 +771,7 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
773
771
|
onCheckedChange={(on) =>
|
|
774
772
|
setVariation(i, { ...v, status: on ? "publish" : "private" })
|
|
775
773
|
}
|
|
776
|
-
aria-label={
|
|
774
|
+
aria-label={t("products.enable_variant", { name: variationLabel(v) })}
|
|
777
775
|
/>
|
|
778
776
|
</td>
|
|
779
777
|
</tr>
|
|
@@ -806,9 +804,9 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
806
804
|
<ConfirmDialog
|
|
807
805
|
open={confirmSingle}
|
|
808
806
|
onOpenChange={setConfirmSingle}
|
|
809
|
-
title="
|
|
810
|
-
description={
|
|
811
|
-
confirmLabel="
|
|
807
|
+
title={t("products.switch_single_title")}
|
|
808
|
+
description={t("products.switch_single_description", { count: variations.length })}
|
|
809
|
+
confirmLabel={t("products.remove_variants")}
|
|
812
810
|
onConfirm={() => {
|
|
813
811
|
up({ attributes: [], default_attributes: [] });
|
|
814
812
|
setVariations([]);
|