@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-539cff4960
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/dist/generated/entities/catalog_product/index.js +16 -0
- package/dist/generated/entities/catalog_product/index.js.map +2 -2
- package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
- package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
- package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
- package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
- package/dist/generated/entities/sales_invoice_line/index.js +7 -1
- package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +6 -0
- package/dist/generated/entities/sales_order_line/index.js.map +2 -2
- package/dist/generated/entities/sales_quote_line/index.js +6 -0
- package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/api/prices/route.js +123 -8
- package/dist/modules/catalog/api/prices/route.js.map +2 -2
- package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
- package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
- package/dist/modules/catalog/api/products/route.js +351 -201
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/commands/index.js +1 -0
- package/dist/modules/catalog/commands/index.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
- package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +355 -73
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +18 -4
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
- package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
- package/dist/modules/catalog/components/products/productForm.js +66 -5
- package/dist/modules/catalog/components/products/productForm.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
- package/dist/modules/catalog/data/entities.js +86 -0
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/catalog/data/validators.js +65 -3
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/events.js +3 -0
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/lib/unitCodes.js +7 -0
- package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
- package/dist/modules/catalog/lib/unitResolution.js +53 -0
- package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
- package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
- package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
- package/dist/modules/catalog/search.js +69 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +91 -42
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/dashboards/seed/analytics.js +3 -0
- package/dist/modules/dashboards/seed/analytics.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +98 -15
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quote-lines/route.js +101 -14
- package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
- package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1424 -260
- package/dist/modules/sales/commands/documents.js.map +3 -3
- package/dist/modules/sales/commands/shared.js +6 -2
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +59 -3
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +35 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
- package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
- package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
- package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
- package/dist/modules/sales/search.js +28 -0
- package/dist/modules/sales/search.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +14 -1
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/translations.js +9 -0
- package/dist/modules/staff/translations.js.map +7 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
- package/dist/modules/translations/lib/extract-record-id.js +31 -2
- package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
- package/dist/modules/translations/lib/resolve-field-list.js +3 -0
- package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
- package/dist/modules/translations/widgets/injection-table.js +18 -29
- package/dist/modules/translations/widgets/injection-table.js.map +2 -2
- package/generated/entities/catalog_product/index.ts +8 -0
- package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
- package/generated/entities/sales_credit_memo_line/index.ts +3 -0
- package/generated/entities/sales_invoice_line/index.ts +3 -0
- package/generated/entities/sales_order_line/index.ts +3 -0
- package/generated/entities/sales_quote_line/index.ts +3 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/auth/i18n/de.json +1 -1
- package/src/modules/auth/i18n/en.json +1 -1
- package/src/modules/auth/i18n/es.json +1 -1
- package/src/modules/auth/i18n/pl.json +1 -1
- package/src/modules/catalog/api/prices/route.ts +213 -81
- package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
- package/src/modules/catalog/api/products/route.ts +638 -402
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
- package/src/modules/catalog/commands/index.ts +1 -0
- package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
- package/src/modules/catalog/commands/products.ts +1151 -693
- package/src/modules/catalog/commands/shared.ts +19 -5
- package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
- package/src/modules/catalog/components/products/productForm.ts +369 -256
- package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
- package/src/modules/catalog/data/entities.ts +82 -1
- package/src/modules/catalog/data/validators.ts +118 -34
- package/src/modules/catalog/events.ts +3 -0
- package/src/modules/catalog/i18n/de.json +56 -0
- package/src/modules/catalog/i18n/en.json +56 -0
- package/src/modules/catalog/i18n/es.json +56 -0
- package/src/modules/catalog/i18n/pl.json +56 -0
- package/src/modules/catalog/lib/unitCodes.ts +1 -0
- package/src/modules/catalog/lib/unitResolution.ts +62 -0
- package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
- package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
- package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
- package/src/modules/catalog/search.ts +73 -1
- package/src/modules/catalog/seed/examples.ts +552 -479
- package/src/modules/dashboards/i18n/de.json +1 -1
- package/src/modules/dashboards/i18n/en.json +1 -1
- package/src/modules/dashboards/i18n/es.json +1 -1
- package/src/modules/dashboards/i18n/pl.json +1 -1
- package/src/modules/dashboards/seed/analytics.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +158 -68
- package/src/modules/sales/api/quote-lines/route.ts +161 -67
- package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
- package/src/modules/sales/commands/documents.ts +4250 -2424
- package/src/modules/sales/commands/shared.ts +7 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
- package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
- package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
- package/src/modules/sales/data/entities.ts +53 -0
- package/src/modules/sales/data/validators.ts +36 -0
- package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
- package/src/modules/sales/i18n/de.json +23 -3
- package/src/modules/sales/i18n/en.json +23 -3
- package/src/modules/sales/i18n/es.json +23 -3
- package/src/modules/sales/i18n/pl.json +23 -3
- package/src/modules/sales/lib/types.ts +30 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
- package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
- package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
- package/src/modules/sales/search.ts +28 -0
- package/src/modules/sales/seed/examples.ts +20 -1
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
- package/src/modules/staff/translations.ts +5 -0
- package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
- package/src/modules/translations/lib/extract-record-id.ts +47 -3
- package/src/modules/translations/lib/resolve-field-list.ts +4 -0
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
- package/src/modules/translations/widgets/injection-table.ts +19 -33
- package/src/modules/workflows/i18n/de.json +4 -4
- package/src/modules/workflows/i18n/en.json +4 -4
- package/src/modules/workflows/i18n/es.json +4 -4
- package/src/modules/workflows/i18n/pl.json +4 -4
|
@@ -1,152 +1,227 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { slugify } from
|
|
3
|
-
import { parseObjectLike } from
|
|
4
|
-
import type {
|
|
5
|
-
import type {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { slugify } from "@open-mercato/shared/lib/slugify";
|
|
3
|
+
import { parseObjectLike } from "@open-mercato/shared/lib/json/parseObjectLike";
|
|
4
|
+
import type { ReferenceUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
|
|
5
|
+
import type { CatalogProductOptionSchema } from "../../data/types";
|
|
6
|
+
import type { ProductMediaItem } from "./ProductMediaManager";
|
|
6
7
|
|
|
7
|
-
export { slugify }
|
|
8
|
+
export { slugify };
|
|
8
9
|
|
|
9
10
|
export type PriceKindSummary = {
|
|
10
|
-
id: string
|
|
11
|
-
code: string
|
|
12
|
-
title: string
|
|
13
|
-
currencyCode: string | null
|
|
14
|
-
displayMode:
|
|
15
|
-
}
|
|
11
|
+
id: string;
|
|
12
|
+
code: string;
|
|
13
|
+
title: string;
|
|
14
|
+
currencyCode: string | null;
|
|
15
|
+
displayMode: "including-tax" | "excluding-tax";
|
|
16
|
+
};
|
|
16
17
|
|
|
17
18
|
export type PriceKindApiPayload = {
|
|
18
|
-
id?: string | number
|
|
19
|
-
code?: string
|
|
20
|
-
title?: string
|
|
21
|
-
currencyCode?: string | null
|
|
22
|
-
currency_code?: string | null
|
|
23
|
-
displayMode?: string | null
|
|
24
|
-
display_mode?: string | null
|
|
25
|
-
}
|
|
19
|
+
id?: string | number;
|
|
20
|
+
code?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
currencyCode?: string | null;
|
|
23
|
+
currency_code?: string | null;
|
|
24
|
+
displayMode?: string | null;
|
|
25
|
+
display_mode?: string | null;
|
|
26
|
+
};
|
|
26
27
|
|
|
27
28
|
export type TaxRateSummary = {
|
|
28
|
-
id: string
|
|
29
|
-
name: string
|
|
30
|
-
code: string | null
|
|
31
|
-
rate: number | null
|
|
32
|
-
isDefault: boolean
|
|
33
|
-
}
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
code: string | null;
|
|
32
|
+
rate: number | null;
|
|
33
|
+
isDefault: boolean;
|
|
34
|
+
};
|
|
34
35
|
|
|
35
36
|
export type ProductOptionInput = {
|
|
36
|
-
id: string
|
|
37
|
-
title: string
|
|
38
|
-
values: Array<{ id: string; label: string }
|
|
39
|
-
}
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
values: Array<{ id: string; label: string }>;
|
|
40
|
+
};
|
|
40
41
|
|
|
41
42
|
export type ProductDimensions = {
|
|
42
|
-
width?: number
|
|
43
|
-
height?: number
|
|
44
|
-
depth?: number
|
|
45
|
-
unit?: string | null
|
|
46
|
-
} | null
|
|
43
|
+
width?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
depth?: number;
|
|
46
|
+
unit?: string | null;
|
|
47
|
+
} | null;
|
|
47
48
|
|
|
48
49
|
export type ProductWeight = {
|
|
49
|
-
value?: number
|
|
50
|
-
unit?: string | null
|
|
51
|
-
} | null
|
|
50
|
+
value?: number;
|
|
51
|
+
unit?: string | null;
|
|
52
|
+
} | null;
|
|
52
53
|
|
|
53
54
|
export type VariantPriceValue = {
|
|
54
|
-
amount: string
|
|
55
|
-
}
|
|
55
|
+
amount: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ProductUnitRoundingMode = "half_up" | "down" | "up";
|
|
59
|
+
export type ProductUnitPriceReferenceUnit = ReferenceUnitCode;
|
|
60
|
+
|
|
61
|
+
export type ProductUnitConversionDraft = {
|
|
62
|
+
id: string | null;
|
|
63
|
+
unitCode: string;
|
|
64
|
+
toBaseFactor: string;
|
|
65
|
+
sortOrder: string;
|
|
66
|
+
isActive: boolean;
|
|
67
|
+
};
|
|
56
68
|
|
|
57
69
|
export type VariantDraft = {
|
|
58
|
-
id: string
|
|
59
|
-
title: string
|
|
60
|
-
sku: string
|
|
61
|
-
isDefault: boolean
|
|
62
|
-
taxRateId: string | null
|
|
63
|
-
manageInventory: boolean
|
|
64
|
-
allowBackorder: boolean
|
|
65
|
-
hasInventoryKit: boolean
|
|
66
|
-
optionValues: Record<string, string
|
|
67
|
-
prices: Record<string, VariantPriceValue
|
|
68
|
-
}
|
|
70
|
+
id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
sku: string;
|
|
73
|
+
isDefault: boolean;
|
|
74
|
+
taxRateId: string | null;
|
|
75
|
+
manageInventory: boolean;
|
|
76
|
+
allowBackorder: boolean;
|
|
77
|
+
hasInventoryKit: boolean;
|
|
78
|
+
optionValues: Record<string, string>;
|
|
79
|
+
prices: Record<string, VariantPriceValue>;
|
|
80
|
+
};
|
|
69
81
|
|
|
70
82
|
export type ProductFormValues = {
|
|
71
|
-
title: string
|
|
72
|
-
subtitle: string
|
|
73
|
-
handle: string
|
|
74
|
-
description: string
|
|
75
|
-
useMarkdown: boolean
|
|
76
|
-
taxRateId: string | null
|
|
77
|
-
mediaDraftId: string
|
|
78
|
-
mediaItems: ProductMediaItem[]
|
|
79
|
-
defaultMediaId: string | null
|
|
80
|
-
defaultMediaUrl: string
|
|
81
|
-
hasVariants: boolean
|
|
82
|
-
options: ProductOptionInput[]
|
|
83
|
-
variants: VariantDraft[]
|
|
84
|
-
metadata?: Record<string, unknown> | null
|
|
85
|
-
dimensions?: ProductDimensions
|
|
86
|
-
weight?: ProductWeight
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
title: string;
|
|
84
|
+
subtitle: string;
|
|
85
|
+
handle: string;
|
|
86
|
+
description: string;
|
|
87
|
+
useMarkdown: boolean;
|
|
88
|
+
taxRateId: string | null;
|
|
89
|
+
mediaDraftId: string;
|
|
90
|
+
mediaItems: ProductMediaItem[];
|
|
91
|
+
defaultMediaId: string | null;
|
|
92
|
+
defaultMediaUrl: string;
|
|
93
|
+
hasVariants: boolean;
|
|
94
|
+
options: ProductOptionInput[];
|
|
95
|
+
variants: VariantDraft[];
|
|
96
|
+
metadata?: Record<string, unknown> | null;
|
|
97
|
+
dimensions?: ProductDimensions;
|
|
98
|
+
weight?: ProductWeight;
|
|
99
|
+
defaultUnit: string | null;
|
|
100
|
+
defaultSalesUnit: string | null;
|
|
101
|
+
defaultSalesUnitQuantity: string;
|
|
102
|
+
uomRoundingScale: string;
|
|
103
|
+
uomRoundingMode: ProductUnitRoundingMode;
|
|
104
|
+
unitPriceEnabled: boolean;
|
|
105
|
+
unitPriceReferenceUnit: string | null;
|
|
106
|
+
unitPriceBaseQuantity: string;
|
|
107
|
+
unitConversions: ProductUnitConversionDraft[];
|
|
108
|
+
customFieldsetCode?: string | null;
|
|
109
|
+
categoryIds: string[];
|
|
110
|
+
channelIds: string[];
|
|
111
|
+
tags: string[];
|
|
112
|
+
optionSchemaId?: string | null;
|
|
113
|
+
};
|
|
93
114
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
mediaDraftId: z.string().optional(),
|
|
108
|
-
mediaItems: z.any().optional(),
|
|
109
|
-
defaultMediaId: z.string().uuid().nullable().optional(),
|
|
110
|
-
defaultMediaUrl: z.string().trim().max(500).nullable().optional(),
|
|
111
|
-
options: z.any().optional(),
|
|
112
|
-
variants: z.any().optional(),
|
|
113
|
-
// Use a permissive schema to avoid zod classic `_zod` runtime crashes on records in edge builds.
|
|
114
|
-
metadata: z.custom<Record<string, unknown>>(() => true).nullable().optional(),
|
|
115
|
-
dimensions: z
|
|
116
|
-
.object({
|
|
117
|
-
width: z.coerce.number().min(0).optional(),
|
|
118
|
-
height: z.coerce.number().min(0).optional(),
|
|
119
|
-
depth: z.coerce.number().min(0).optional(),
|
|
120
|
-
unit: z.string().trim().max(25).optional(),
|
|
121
|
-
})
|
|
122
|
-
.nullable()
|
|
123
|
-
.optional(),
|
|
124
|
-
weight: z
|
|
125
|
-
.object({
|
|
126
|
-
value: z.coerce.number().min(0).optional(),
|
|
127
|
-
unit: z.string().trim().max(25).optional(),
|
|
128
|
-
})
|
|
129
|
-
.nullable()
|
|
130
|
-
.optional(),
|
|
131
|
-
customFieldsetCode: z.string().optional().nullable(),
|
|
132
|
-
categoryIds: z.array(z.string().uuid()).optional(),
|
|
133
|
-
channelIds: z.array(z.string().uuid()).optional(),
|
|
134
|
-
tags: z.array(z.string().trim().min(1).max(100)).optional(),
|
|
135
|
-
optionSchemaId: z.string().uuid().nullable().optional(),
|
|
136
|
-
}).passthrough()
|
|
115
|
+
const optionalPositiveNumberInput = z.preprocess((value) => {
|
|
116
|
+
if (value === null || value === undefined) return undefined;
|
|
117
|
+
if (typeof value === "string" && value.trim().length === 0) return undefined;
|
|
118
|
+
return value;
|
|
119
|
+
}, z.coerce.number().positive().optional());
|
|
120
|
+
|
|
121
|
+
const optionalBoundedIntegerInput = (min: number, max: number) =>
|
|
122
|
+
z.preprocess((value) => {
|
|
123
|
+
if (value === null || value === undefined) return undefined;
|
|
124
|
+
if (typeof value === "string" && value.trim().length === 0)
|
|
125
|
+
return undefined;
|
|
126
|
+
return value;
|
|
127
|
+
}, z.coerce.number().int().min(min).max(max).optional());
|
|
137
128
|
|
|
138
|
-
export const
|
|
129
|
+
export const productFormSchema = z
|
|
130
|
+
.object({
|
|
131
|
+
title: z.string().trim().min(1, "catalog.products.validation.titleRequired"),
|
|
132
|
+
subtitle: z.string().optional(),
|
|
133
|
+
handle: z
|
|
134
|
+
.string()
|
|
135
|
+
.trim()
|
|
136
|
+
.regex(
|
|
137
|
+
/^[a-z0-9\-_]*$/,
|
|
138
|
+
"catalog.products.validation.handleFormat",
|
|
139
|
+
)
|
|
140
|
+
.max(150)
|
|
141
|
+
.optional(),
|
|
142
|
+
description: z.string().optional(),
|
|
143
|
+
useMarkdown: z.boolean().optional(),
|
|
144
|
+
taxRateId: z.string().uuid().nullable().optional(),
|
|
145
|
+
hasVariants: z.boolean().optional(),
|
|
146
|
+
mediaDraftId: z.string().optional(),
|
|
147
|
+
mediaItems: z.any().optional(),
|
|
148
|
+
defaultMediaId: z.string().uuid().nullable().optional(),
|
|
149
|
+
defaultMediaUrl: z.string().trim().max(500).nullable().optional(),
|
|
150
|
+
options: z.any().optional(),
|
|
151
|
+
variants: z.any().optional(),
|
|
152
|
+
// Use a permissive schema to avoid zod classic `_zod` runtime crashes on records in edge builds.
|
|
153
|
+
metadata: z
|
|
154
|
+
.custom<Record<string, unknown>>(() => true)
|
|
155
|
+
.nullable()
|
|
156
|
+
.optional(),
|
|
157
|
+
dimensions: z
|
|
158
|
+
.object({
|
|
159
|
+
width: z.coerce.number().min(0).optional(),
|
|
160
|
+
height: z.coerce.number().min(0).optional(),
|
|
161
|
+
depth: z.coerce.number().min(0).optional(),
|
|
162
|
+
unit: z.string().trim().max(25).optional(),
|
|
163
|
+
})
|
|
164
|
+
.nullable()
|
|
165
|
+
.optional(),
|
|
166
|
+
weight: z
|
|
167
|
+
.object({
|
|
168
|
+
value: z.coerce.number().min(0).optional(),
|
|
169
|
+
unit: z.string().trim().max(25).optional(),
|
|
170
|
+
})
|
|
171
|
+
.nullable()
|
|
172
|
+
.optional(),
|
|
173
|
+
defaultUnit: z.string().trim().max(50).nullable().optional(),
|
|
174
|
+
defaultSalesUnit: z.string().trim().max(50).nullable().optional(),
|
|
175
|
+
defaultSalesUnitQuantity: optionalPositiveNumberInput,
|
|
176
|
+
uomRoundingScale: optionalBoundedIntegerInput(0, 6),
|
|
177
|
+
uomRoundingMode: z.enum(["half_up", "down", "up"]).optional(),
|
|
178
|
+
unitPriceEnabled: z.boolean().optional(),
|
|
179
|
+
unitPriceReferenceUnit: z.string().trim().max(50).nullable().optional(),
|
|
180
|
+
unitPriceBaseQuantity: optionalPositiveNumberInput,
|
|
181
|
+
unitConversions: z
|
|
182
|
+
.array(
|
|
183
|
+
z.object({
|
|
184
|
+
id: z.string().nullable().optional(),
|
|
185
|
+
unitCode: z.string().trim().max(50),
|
|
186
|
+
toBaseFactor: z.coerce.number().positive(),
|
|
187
|
+
sortOrder: z.coerce.number().int().min(0).max(100000).optional(),
|
|
188
|
+
isActive: z.boolean().optional(),
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
.optional(),
|
|
192
|
+
customFieldsetCode: z.string().optional().nullable(),
|
|
193
|
+
categoryIds: z.array(z.string().uuid()).optional(),
|
|
194
|
+
channelIds: z.array(z.string().uuid()).optional(),
|
|
195
|
+
tags: z.array(z.string().trim().min(1).max(100)).optional(),
|
|
196
|
+
optionSchemaId: z.string().uuid().nullable().optional(),
|
|
197
|
+
})
|
|
198
|
+
.passthrough()
|
|
199
|
+
.refine(
|
|
200
|
+
(data) => !data.unitPriceEnabled || (data.unitPriceReferenceUnit != null && data.unitPriceReferenceUnit.length > 0),
|
|
201
|
+
{ message: 'catalog.products.validation.referenceUnitRequired', path: ['unitPriceReferenceUnit'] }
|
|
202
|
+
)
|
|
203
|
+
.refine(
|
|
204
|
+
(data) => !data.defaultSalesUnit || (data.defaultUnit != null && data.defaultUnit.length > 0),
|
|
205
|
+
{ message: 'catalog.products.validation.baseUnitRequired', path: ['defaultUnit'] }
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export const PRODUCT_FORM_STEPS = [
|
|
209
|
+
"general",
|
|
210
|
+
"organize",
|
|
211
|
+
"uom",
|
|
212
|
+
"variants",
|
|
213
|
+
] as const;
|
|
139
214
|
|
|
140
215
|
export const BASE_INITIAL_VALUES: ProductFormValues = {
|
|
141
|
-
title:
|
|
142
|
-
subtitle:
|
|
143
|
-
handle:
|
|
144
|
-
description:
|
|
216
|
+
title: "",
|
|
217
|
+
subtitle: "",
|
|
218
|
+
handle: "",
|
|
219
|
+
description: "",
|
|
145
220
|
useMarkdown: false,
|
|
146
|
-
mediaDraftId:
|
|
221
|
+
mediaDraftId: "",
|
|
147
222
|
mediaItems: [],
|
|
148
223
|
defaultMediaId: null,
|
|
149
|
-
defaultMediaUrl:
|
|
224
|
+
defaultMediaUrl: "",
|
|
150
225
|
taxRateId: null,
|
|
151
226
|
hasVariants: false,
|
|
152
227
|
options: [],
|
|
@@ -154,26 +229,35 @@ export const BASE_INITIAL_VALUES: ProductFormValues = {
|
|
|
154
229
|
metadata: {},
|
|
155
230
|
dimensions: null,
|
|
156
231
|
weight: null,
|
|
232
|
+
defaultUnit: null,
|
|
233
|
+
defaultSalesUnit: null,
|
|
234
|
+
defaultSalesUnitQuantity: "1",
|
|
235
|
+
uomRoundingScale: "4",
|
|
236
|
+
uomRoundingMode: "half_up",
|
|
237
|
+
unitPriceEnabled: false,
|
|
238
|
+
unitPriceReferenceUnit: null,
|
|
239
|
+
unitPriceBaseQuantity: "",
|
|
240
|
+
unitConversions: [],
|
|
157
241
|
customFieldsetCode: null,
|
|
158
242
|
categoryIds: [],
|
|
159
243
|
channelIds: [],
|
|
160
244
|
tags: [],
|
|
161
245
|
optionSchemaId: null,
|
|
162
|
-
}
|
|
246
|
+
};
|
|
163
247
|
|
|
164
248
|
export const createInitialProductFormValues = (): ProductFormValues => ({
|
|
165
249
|
...BASE_INITIAL_VALUES,
|
|
166
250
|
mediaDraftId: createLocalId(),
|
|
167
251
|
variants: [createVariantDraft(null, { isDefault: true })],
|
|
168
|
-
})
|
|
252
|
+
});
|
|
169
253
|
|
|
170
254
|
export const createVariantDraft = (
|
|
171
255
|
productTaxRateId: string | null,
|
|
172
256
|
overrides: Partial<VariantDraft> = {},
|
|
173
257
|
): VariantDraft => ({
|
|
174
258
|
id: createLocalId(),
|
|
175
|
-
title:
|
|
176
|
-
sku:
|
|
259
|
+
title: "Default variant",
|
|
260
|
+
sku: "",
|
|
177
261
|
isDefault: false,
|
|
178
262
|
taxRateId: productTaxRateId ?? null,
|
|
179
263
|
manageInventory: false,
|
|
@@ -182,224 +266,253 @@ export const createVariantDraft = (
|
|
|
182
266
|
optionValues: {},
|
|
183
267
|
prices: {},
|
|
184
268
|
...overrides,
|
|
185
|
-
})
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
export const createProductUnitConversionDraft = (
|
|
272
|
+
overrides: Partial<ProductUnitConversionDraft> = {},
|
|
273
|
+
): ProductUnitConversionDraft => ({
|
|
274
|
+
id: null,
|
|
275
|
+
unitCode: "",
|
|
276
|
+
toBaseFactor: "",
|
|
277
|
+
sortOrder: "",
|
|
278
|
+
isActive: true,
|
|
279
|
+
...overrides,
|
|
280
|
+
});
|
|
186
281
|
|
|
187
|
-
export const buildOptionValuesKey = (
|
|
188
|
-
|
|
282
|
+
export const buildOptionValuesKey = (
|
|
283
|
+
optionValues?: Record<string, string>,
|
|
284
|
+
): string => {
|
|
285
|
+
if (!optionValues) return "";
|
|
189
286
|
return Object.keys(optionValues)
|
|
190
287
|
.sort()
|
|
191
|
-
.map((key) => `${key}:${optionValues[key] ??
|
|
192
|
-
.join(
|
|
193
|
-
}
|
|
288
|
+
.map((key) => `${key}:${optionValues[key] ?? ""}`)
|
|
289
|
+
.join("|");
|
|
290
|
+
};
|
|
194
291
|
|
|
195
292
|
export const haveSameOptionValues = (
|
|
196
293
|
current: Record<string, string> | undefined,
|
|
197
294
|
next: Record<string, string>,
|
|
198
295
|
): boolean => {
|
|
199
|
-
const a = current ?? {}
|
|
200
|
-
const keys = new Set([...Object.keys(a), ...Object.keys(next)])
|
|
296
|
+
const a = current ?? {};
|
|
297
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(next)]);
|
|
201
298
|
for (const key of keys) {
|
|
202
|
-
if ((a[key] ??
|
|
299
|
+
if ((a[key] ?? "") !== (next[key] ?? "")) return false;
|
|
203
300
|
}
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
301
|
+
return true;
|
|
302
|
+
};
|
|
206
303
|
|
|
207
304
|
const parseNumeric = (input: unknown): number | null => {
|
|
208
|
-
const numeric = typeof input ===
|
|
209
|
-
if (!Number.isFinite(numeric) || numeric < 0) return null
|
|
210
|
-
return numeric
|
|
211
|
-
}
|
|
305
|
+
const numeric = typeof input === "number" ? input : Number(input);
|
|
306
|
+
if (!Number.isFinite(numeric) || numeric < 0) return null;
|
|
307
|
+
return numeric;
|
|
308
|
+
};
|
|
212
309
|
|
|
213
310
|
export const normalizeProductDimensions = (raw: unknown): ProductDimensions => {
|
|
214
|
-
const source = parseObjectLike(raw)
|
|
215
|
-
if (!source) return null
|
|
216
|
-
const width = parseNumeric(source.width)
|
|
217
|
-
const height = parseNumeric(source.height)
|
|
218
|
-
const depth = parseNumeric(source.depth)
|
|
219
|
-
const unit =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
311
|
+
const source = parseObjectLike(raw);
|
|
312
|
+
if (!source) return null;
|
|
313
|
+
const width = parseNumeric(source.width);
|
|
314
|
+
const height = parseNumeric(source.height);
|
|
315
|
+
const depth = parseNumeric(source.depth);
|
|
316
|
+
const unit =
|
|
317
|
+
typeof source.unit === "string" && source.unit.trim().length
|
|
318
|
+
? source.unit.trim()
|
|
319
|
+
: null;
|
|
320
|
+
const clean: Record<string, unknown> = {};
|
|
321
|
+
if (width !== null) clean.width = width;
|
|
322
|
+
if (height !== null) clean.height = height;
|
|
323
|
+
if (depth !== null) clean.depth = depth;
|
|
324
|
+
if (unit) clean.unit = unit;
|
|
325
|
+
return Object.keys(clean).length ? (clean as ProductDimensions) : null;
|
|
326
|
+
};
|
|
227
327
|
|
|
228
328
|
export const normalizeProductWeight = (raw: unknown): ProductWeight => {
|
|
229
|
-
const source = parseObjectLike(raw)
|
|
230
|
-
if (!source) return null
|
|
231
|
-
const value = parseNumeric(source.value)
|
|
232
|
-
const unit =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (unit)
|
|
237
|
-
|
|
238
|
-
|
|
329
|
+
const source = parseObjectLike(raw);
|
|
330
|
+
if (!source) return null;
|
|
331
|
+
const value = parseNumeric(source.value);
|
|
332
|
+
const unit =
|
|
333
|
+
typeof source.unit === "string" && source.unit.trim().length
|
|
334
|
+
? source.unit.trim()
|
|
335
|
+
: null;
|
|
336
|
+
if (value === null && !unit) return null;
|
|
337
|
+
const clean: Record<string, unknown> = {};
|
|
338
|
+
if (value !== null) clean.value = value;
|
|
339
|
+
if (unit) clean.unit = unit;
|
|
340
|
+
return clean as ProductWeight;
|
|
341
|
+
};
|
|
239
342
|
|
|
240
|
-
export const sanitizeProductDimensions = (
|
|
241
|
-
|
|
242
|
-
|
|
343
|
+
export const sanitizeProductDimensions = (
|
|
344
|
+
raw: ProductDimensions,
|
|
345
|
+
): ProductDimensions => {
|
|
346
|
+
return normalizeProductDimensions(raw ?? null);
|
|
347
|
+
};
|
|
243
348
|
|
|
244
349
|
export const sanitizeProductWeight = (raw: ProductWeight): ProductWeight => {
|
|
245
|
-
return normalizeProductWeight(raw ?? null)
|
|
246
|
-
}
|
|
350
|
+
return normalizeProductWeight(raw ?? null);
|
|
351
|
+
};
|
|
247
352
|
|
|
248
353
|
export const updateDimensionValue = (
|
|
249
354
|
current: ProductDimensions,
|
|
250
|
-
field:
|
|
251
|
-
raw: string
|
|
355
|
+
field: "width" | "height" | "depth" | "unit",
|
|
356
|
+
raw: string,
|
|
252
357
|
): ProductDimensions => {
|
|
253
|
-
const base = normalizeProductDimensions(current) ?? {}
|
|
254
|
-
if (field ===
|
|
255
|
-
base.unit = raw
|
|
358
|
+
const base = normalizeProductDimensions(current) ?? {};
|
|
359
|
+
if (field === "unit") {
|
|
360
|
+
base.unit = raw;
|
|
256
361
|
} else {
|
|
257
|
-
const numeric = parseNumeric(raw)
|
|
362
|
+
const numeric = parseNumeric(raw);
|
|
258
363
|
if (numeric === null) {
|
|
259
|
-
delete base[field]
|
|
364
|
+
delete base[field];
|
|
260
365
|
} else {
|
|
261
|
-
base[field] = numeric
|
|
366
|
+
base[field] = numeric;
|
|
262
367
|
}
|
|
263
368
|
}
|
|
264
|
-
return sanitizeProductDimensions(base)
|
|
265
|
-
}
|
|
369
|
+
return sanitizeProductDimensions(base);
|
|
370
|
+
};
|
|
266
371
|
|
|
267
372
|
export const updateWeightValue = (
|
|
268
373
|
current: ProductWeight,
|
|
269
|
-
field:
|
|
270
|
-
raw: string
|
|
374
|
+
field: "value" | "unit",
|
|
375
|
+
raw: string,
|
|
271
376
|
): ProductWeight => {
|
|
272
|
-
const base = normalizeProductWeight(current) ?? {}
|
|
273
|
-
if (field ===
|
|
274
|
-
base.unit = raw
|
|
377
|
+
const base = normalizeProductWeight(current) ?? {};
|
|
378
|
+
if (field === "unit") {
|
|
379
|
+
base.unit = raw;
|
|
275
380
|
} else {
|
|
276
|
-
const numeric = parseNumeric(raw)
|
|
381
|
+
const numeric = parseNumeric(raw);
|
|
277
382
|
if (numeric === null) {
|
|
278
|
-
delete (base as Record<string, unknown>).value
|
|
383
|
+
delete (base as Record<string, unknown>).value;
|
|
279
384
|
} else {
|
|
280
|
-
base.value = numeric
|
|
385
|
+
base.value = numeric;
|
|
281
386
|
}
|
|
282
387
|
}
|
|
283
|
-
return sanitizeProductWeight(base)
|
|
284
|
-
}
|
|
388
|
+
return sanitizeProductWeight(base);
|
|
389
|
+
};
|
|
285
390
|
|
|
286
|
-
export const normalizePriceKindSummary = (
|
|
287
|
-
|
|
391
|
+
export const normalizePriceKindSummary = (
|
|
392
|
+
input: PriceKindApiPayload | undefined | null,
|
|
393
|
+
): PriceKindSummary | null => {
|
|
394
|
+
if (!input) return null;
|
|
288
395
|
const getString = (value: unknown): string | null => {
|
|
289
|
-
if (typeof value ===
|
|
290
|
-
if (typeof value ===
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
396
|
+
if (typeof value === "string" && value.trim().length) return value.trim();
|
|
397
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
398
|
+
return String(value);
|
|
399
|
+
return null;
|
|
400
|
+
};
|
|
401
|
+
const id = getString(input.id);
|
|
402
|
+
const code = getString(input.code);
|
|
403
|
+
const title = getString(input.title);
|
|
404
|
+
if (!id || !code || !title) return null;
|
|
405
|
+
const currency =
|
|
406
|
+
getString(input.currencyCode) ?? getString(input.currency_code);
|
|
407
|
+
const displayRaw =
|
|
408
|
+
getString(input.displayMode) ?? getString(input.display_mode);
|
|
409
|
+
const displayMode: PriceKindSummary["displayMode"] =
|
|
410
|
+
displayRaw === "including-tax" ? "including-tax" : "excluding-tax";
|
|
301
411
|
return {
|
|
302
412
|
id,
|
|
303
413
|
code,
|
|
304
414
|
title,
|
|
305
415
|
currencyCode: currency,
|
|
306
416
|
displayMode,
|
|
307
|
-
}
|
|
308
|
-
}
|
|
417
|
+
};
|
|
418
|
+
};
|
|
309
419
|
|
|
310
420
|
export const formatTaxRateLabel = (rate: TaxRateSummary): string => {
|
|
311
|
-
const extras: string[] = []
|
|
312
|
-
if (typeof rate.rate ===
|
|
313
|
-
extras.push(`${rate.rate}%`)
|
|
421
|
+
const extras: string[] = [];
|
|
422
|
+
if (typeof rate.rate === "number" && Number.isFinite(rate.rate)) {
|
|
423
|
+
extras.push(`${rate.rate}%`);
|
|
314
424
|
}
|
|
315
425
|
if (rate.code) {
|
|
316
|
-
extras.push(rate.code.toUpperCase())
|
|
426
|
+
extras.push(rate.code.toUpperCase());
|
|
317
427
|
}
|
|
318
|
-
if (!extras.length) return rate.name
|
|
319
|
-
return `${rate.name} • ${extras.join(
|
|
320
|
-
}
|
|
428
|
+
if (!extras.length) return rate.name;
|
|
429
|
+
return `${rate.name} • ${extras.join(" · ")}`;
|
|
430
|
+
};
|
|
321
431
|
|
|
322
432
|
export function createLocalId(): string {
|
|
323
|
-
return Math.random().toString(36).slice(2, 10)
|
|
433
|
+
return Math.random().toString(36).slice(2, 10);
|
|
324
434
|
}
|
|
325
435
|
|
|
326
436
|
export function buildOptionSchemaDefinition(
|
|
327
437
|
options: ProductOptionInput[] | undefined,
|
|
328
|
-
name: string
|
|
438
|
+
name: string,
|
|
329
439
|
): CatalogProductOptionSchema | null {
|
|
330
|
-
const list = Array.isArray(options) ? options : []
|
|
331
|
-
if (!list.length) return null
|
|
332
|
-
const normalizedName =
|
|
440
|
+
const list = Array.isArray(options) ? options : [];
|
|
441
|
+
if (!list.length) return null;
|
|
442
|
+
const normalizedName =
|
|
443
|
+
name && name.trim().length ? name.trim() : "Product options";
|
|
333
444
|
const schemaOptions = list
|
|
334
445
|
.map((option) => {
|
|
335
|
-
const title = option.title?.trim() ||
|
|
336
|
-
const code = resolveOptionCode(option)
|
|
337
|
-
const values = Array.isArray(option.values) ? option.values : []
|
|
446
|
+
const title = option.title?.trim() || "";
|
|
447
|
+
const code = resolveOptionCode(option);
|
|
448
|
+
const values = Array.isArray(option.values) ? option.values : [];
|
|
338
449
|
return {
|
|
339
450
|
code: code || slugify(createLocalId()),
|
|
340
|
-
label: title || code ||
|
|
341
|
-
inputType:
|
|
451
|
+
label: title || code || "Option",
|
|
452
|
+
inputType: "select" as const,
|
|
342
453
|
choices: values
|
|
343
454
|
.map((value) => {
|
|
344
|
-
const label = value.label?.trim() ||
|
|
345
|
-
const valueCode = slugify(label || value.id || createLocalId())
|
|
346
|
-
if (!label && !valueCode) return null
|
|
455
|
+
const label = value.label?.trim() || "";
|
|
456
|
+
const valueCode = slugify(label || value.id || createLocalId());
|
|
457
|
+
if (!label && !valueCode) return null;
|
|
347
458
|
return {
|
|
348
459
|
code: valueCode || slugify(createLocalId()),
|
|
349
|
-
label: label || valueCode ||
|
|
350
|
-
}
|
|
460
|
+
label: label || valueCode || "Choice",
|
|
461
|
+
};
|
|
351
462
|
})
|
|
352
463
|
.filter((entry): entry is { code: string; label: string } => !!entry),
|
|
353
|
-
}
|
|
464
|
+
};
|
|
354
465
|
})
|
|
355
|
-
.filter((entry) => entry.label.trim().length)
|
|
356
|
-
if (!schemaOptions.length) return null
|
|
466
|
+
.filter((entry) => entry.label.trim().length);
|
|
467
|
+
if (!schemaOptions.length) return null;
|
|
357
468
|
return {
|
|
358
469
|
version: 1,
|
|
359
470
|
name: normalizedName,
|
|
360
471
|
options: schemaOptions,
|
|
361
|
-
}
|
|
472
|
+
};
|
|
362
473
|
}
|
|
363
474
|
|
|
364
475
|
export function convertSchemaToProductOptions(
|
|
365
|
-
schema: CatalogProductOptionSchema | null | undefined
|
|
476
|
+
schema: CatalogProductOptionSchema | null | undefined,
|
|
366
477
|
): ProductOptionInput[] {
|
|
367
|
-
if (!schema || !Array.isArray(schema.options)) return []
|
|
478
|
+
if (!schema || !Array.isArray(schema.options)) return [];
|
|
368
479
|
return schema.options.map((option) => ({
|
|
369
480
|
id: createLocalId(),
|
|
370
|
-
title: option.label ?? option.code ??
|
|
481
|
+
title: option.label ?? option.code ?? "Option",
|
|
371
482
|
values: Array.isArray(option.choices)
|
|
372
483
|
? option.choices.map((choice) => ({
|
|
373
484
|
id: createLocalId(),
|
|
374
|
-
label: choice.label ?? choice.code ??
|
|
485
|
+
label: choice.label ?? choice.code ?? "",
|
|
375
486
|
}))
|
|
376
487
|
: [],
|
|
377
|
-
}))
|
|
488
|
+
}));
|
|
378
489
|
}
|
|
379
490
|
|
|
380
491
|
function resolveOptionCode(option: ProductOptionInput): string {
|
|
381
|
-
const base = option.title?.trim() || option.id?.trim() ||
|
|
382
|
-
const slugged = slugify(base)
|
|
383
|
-
if (slugged.length) return slugged
|
|
384
|
-
if (base.length) return base
|
|
385
|
-
return createLocalId()
|
|
492
|
+
const base = option.title?.trim() || option.id?.trim() || "";
|
|
493
|
+
const slugged = slugify(base);
|
|
494
|
+
if (slugged.length) return slugged;
|
|
495
|
+
if (base.length) return base;
|
|
496
|
+
return createLocalId();
|
|
386
497
|
}
|
|
387
498
|
|
|
388
|
-
export function buildVariantCombinations(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (!
|
|
392
|
-
const
|
|
393
|
-
|
|
499
|
+
export function buildVariantCombinations(
|
|
500
|
+
options: ProductOptionInput[],
|
|
501
|
+
): Record<string, string>[] {
|
|
502
|
+
if (!options.length) return [];
|
|
503
|
+
const [first, ...rest] = options;
|
|
504
|
+
if (!first || !Array.isArray(first.values) || !first.values.length) return [];
|
|
505
|
+
const firstKey = resolveOptionCode(first);
|
|
506
|
+
const initial = first.values.map((value) => ({ [firstKey]: value.label }));
|
|
394
507
|
return rest.reduce<Record<string, string>[]>((acc, option) => {
|
|
395
|
-
if (!Array.isArray(option.values) || !option.values.length) return []
|
|
396
|
-
const optionKey = resolveOptionCode(option)
|
|
397
|
-
const combos: Record<string, string>[] = []
|
|
508
|
+
if (!Array.isArray(option.values) || !option.values.length) return [];
|
|
509
|
+
const optionKey = resolveOptionCode(option);
|
|
510
|
+
const combos: Record<string, string>[] = [];
|
|
398
511
|
acc.forEach((partial) => {
|
|
399
512
|
option.values.forEach((value) => {
|
|
400
|
-
combos.push({ ...partial, [optionKey]: value.label })
|
|
401
|
-
})
|
|
402
|
-
})
|
|
403
|
-
return combos
|
|
404
|
-
}, initial)
|
|
513
|
+
combos.push({ ...partial, [optionKey]: value.label });
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
return combos;
|
|
517
|
+
}, initial);
|
|
405
518
|
}
|