@vendure/dashboard 3.4.2-master-202508290230 → 3.4.2-master-202509030226
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 +4 -4
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +3 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +7 -7
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +3 -3
- package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -4
- package/src/app/routes/_authenticated/_products/components/add-option-group-dialog.tsx +127 -0
- package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +41 -39
- package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +1 -33
- package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +7 -42
- package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +38 -134
- package/src/app/routes/_authenticated/_products/components/option-groups-editor.tsx +180 -0
- package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +9 -39
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_products/products.graphql.ts +136 -0
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -9
- package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +405 -0
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +3 -3
- package/src/lib/components/data-input/rich-text-input.tsx +8 -4
- package/src/lib/components/layout/channel-switcher.tsx +27 -6
- package/src/lib/components/layout/manage-languages-dialog.tsx +2 -2
- package/src/lib/components/shared/asset/asset-gallery.tsx +20 -2
- package/src/lib/components/shared/asset/asset-picker-dialog.tsx +5 -5
- package/src/lib/components/shared/assign-to-channel-dialog.tsx +2 -2
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -2
- package/src/lib/graphql/api.ts +3 -1
- package/src/lib/hooks/use-permissions.ts +4 -4
- package/src/lib/providers/auth.tsx +8 -0
- package/src/lib/providers/channel-provider.tsx +48 -57
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Alert, AlertDescription } from '@/vdb/components/ui/alert.js';
|
|
2
|
-
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
2
|
import { Checkbox } from '@/vdb/components/ui/checkbox.js';
|
|
4
3
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/vdb/components/ui/form.js';
|
|
5
4
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
@@ -9,11 +8,10 @@ import { graphql } from '@/vdb/graphql/graphql.js';
|
|
|
9
8
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
10
9
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
11
10
|
import { useQuery } from '@tanstack/react-query';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
|
|
11
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
12
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
15
13
|
import { z } from 'zod';
|
|
16
|
-
import {
|
|
14
|
+
import { OptionGroupConfiguration, optionGroupSchema, OptionGroupsEditor } from './option-groups-editor.js';
|
|
17
15
|
|
|
18
16
|
const getStockLocationsDocument = graphql(`
|
|
19
17
|
query GetStockLocations($options: StockLocationListOptions) {
|
|
@@ -27,17 +25,6 @@ const getStockLocationsDocument = graphql(`
|
|
|
27
25
|
}
|
|
28
26
|
`);
|
|
29
27
|
|
|
30
|
-
// Define schemas for validation
|
|
31
|
-
const optionValueSchema = z.object({
|
|
32
|
-
value: z.string().min(1, { message: 'Value cannot be empty' }),
|
|
33
|
-
id: z.string().min(1, { message: 'Value cannot be empty' }),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const optionGroupSchema = z.object({
|
|
37
|
-
name: z.string().min(1, { message: 'Option name is required' }),
|
|
38
|
-
values: z.array(optionValueSchema).min(1, { message: 'At least one value is required' }),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
28
|
type VariantOption = {
|
|
42
29
|
name: string;
|
|
43
30
|
value: string;
|
|
@@ -88,9 +75,7 @@ const formSchema = z.object({
|
|
|
88
75
|
variants: z.record(variantSchema),
|
|
89
76
|
});
|
|
90
77
|
|
|
91
|
-
type OptionGroupForm = z.infer<typeof optionGroupSchema>;
|
|
92
78
|
type VariantForm = z.infer<typeof variantSchema>;
|
|
93
|
-
type FormValues = z.infer<typeof formSchema>;
|
|
94
79
|
|
|
95
80
|
interface CreateProductVariantsProps {
|
|
96
81
|
currencyCode?: string;
|
|
@@ -107,80 +92,52 @@ export function CreateProductVariants({
|
|
|
107
92
|
});
|
|
108
93
|
const stockLocations = stockLocationsResult?.stockLocations.items ?? [];
|
|
109
94
|
|
|
110
|
-
const
|
|
111
|
-
|
|
95
|
+
const [optionGroups, setOptionGroups] = useState<OptionGroupConfiguration['optionGroups']>([]);
|
|
96
|
+
|
|
97
|
+
const form = useForm<{ variants: Record<string, VariantForm> }>({
|
|
98
|
+
resolver: zodResolver(z.object({ variants: z.record(variantSchema) })),
|
|
112
99
|
defaultValues: {
|
|
113
|
-
optionGroups: [],
|
|
114
100
|
variants: {},
|
|
115
101
|
},
|
|
116
102
|
mode: 'onChange',
|
|
117
103
|
});
|
|
118
104
|
|
|
119
|
-
const {
|
|
120
|
-
const {
|
|
121
|
-
fields: optionGroups,
|
|
122
|
-
append: appendOptionGroup,
|
|
123
|
-
remove: removeOptionGroup,
|
|
124
|
-
} = useFieldArray({
|
|
125
|
-
control,
|
|
126
|
-
name: 'optionGroups',
|
|
127
|
-
});
|
|
105
|
+
const { setValue } = form;
|
|
128
106
|
|
|
129
|
-
const watchedOptionGroups = watch('optionGroups');
|
|
130
107
|
// memoize the variants
|
|
131
|
-
const variants = useMemo(
|
|
132
|
-
() => generateVariants(watchedOptionGroups),
|
|
133
|
-
[JSON.stringify(watchedOptionGroups)],
|
|
134
|
-
);
|
|
108
|
+
const variants = useMemo(() => generateVariants(optionGroups), [JSON.stringify(optionGroups)]);
|
|
135
109
|
|
|
136
110
|
// Use the handleSubmit approach for the entire form
|
|
137
111
|
useEffect(() => {
|
|
138
|
-
const subscription = form.watch(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
});
|
|
154
|
-
}
|
|
112
|
+
const subscription = form.watch(value => {
|
|
113
|
+
const formVariants = value?.variants || {};
|
|
114
|
+
const activeVariants: VariantConfiguration['variants'] = [];
|
|
115
|
+
|
|
116
|
+
variants.forEach(variant => {
|
|
117
|
+
if (variant && typeof variant === 'object') {
|
|
118
|
+
const formVariant = formVariants[variant.id];
|
|
119
|
+
if (formVariant) {
|
|
120
|
+
activeVariants.push({
|
|
121
|
+
enabled: formVariant.enabled ?? true,
|
|
122
|
+
sku: formVariant.sku ?? '',
|
|
123
|
+
price: formVariant.price ?? '',
|
|
124
|
+
stock: formVariant.stock ?? '',
|
|
125
|
+
options: variant.options,
|
|
126
|
+
});
|
|
155
127
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const validOptionGroups = value.optionGroups
|
|
159
|
-
.filter((group): group is NonNullable<typeof group> => !!group)
|
|
160
|
-
.filter(group => typeof group.name === 'string' && Array.isArray(group.values))
|
|
161
|
-
.map(group => ({
|
|
162
|
-
name: group.name,
|
|
163
|
-
values: (group.values || [])
|
|
164
|
-
.filter((v): v is NonNullable<typeof v> => !!v)
|
|
165
|
-
.filter(v => typeof v.value === 'string' && typeof v.id === 'string')
|
|
166
|
-
.map(v => ({
|
|
167
|
-
value: v.value,
|
|
168
|
-
id: v.id,
|
|
169
|
-
})),
|
|
170
|
-
}))
|
|
171
|
-
.filter(group => group.values.length > 0) as VariantConfiguration['optionGroups'];
|
|
172
|
-
|
|
173
|
-
const filteredData: VariantConfiguration = {
|
|
174
|
-
optionGroups: validOptionGroups,
|
|
175
|
-
variants: activeVariants,
|
|
176
|
-
};
|
|
128
|
+
}
|
|
129
|
+
});
|
|
177
130
|
|
|
178
|
-
|
|
179
|
-
|
|
131
|
+
const filteredData: VariantConfiguration = {
|
|
132
|
+
optionGroups,
|
|
133
|
+
variants: activeVariants,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
onChange?.({ data: filteredData });
|
|
180
137
|
});
|
|
181
138
|
|
|
182
139
|
return () => subscription.unsubscribe();
|
|
183
|
-
}, [form, onChange, variants]);
|
|
140
|
+
}, [form, onChange, variants, optionGroups]);
|
|
184
141
|
|
|
185
142
|
// Initialize variant form values when variants change
|
|
186
143
|
useEffect(() => {
|
|
@@ -202,64 +159,11 @@ export function CreateProductVariants({
|
|
|
202
159
|
setValue('variants', updatedVariants);
|
|
203
160
|
}, [variants, form, setValue]);
|
|
204
161
|
|
|
205
|
-
const handleAddOptionGroup = () => {
|
|
206
|
-
appendOptionGroup({ name: '', values: [] });
|
|
207
|
-
};
|
|
208
|
-
|
|
209
162
|
return (
|
|
210
163
|
<FormProvider {...form}>
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
<FormField
|
|
215
|
-
control={form.control}
|
|
216
|
-
name={`optionGroups.${index}.name`}
|
|
217
|
-
render={({ field }) => (
|
|
218
|
-
<FormItem>
|
|
219
|
-
<FormLabel>
|
|
220
|
-
<Trans>Option</Trans>
|
|
221
|
-
</FormLabel>
|
|
222
|
-
<FormControl>
|
|
223
|
-
<Input placeholder="e.g. Size" {...field} />
|
|
224
|
-
</FormControl>
|
|
225
|
-
<FormMessage />
|
|
226
|
-
</FormItem>
|
|
227
|
-
)}
|
|
228
|
-
/>
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
<div>
|
|
232
|
-
<FormItem>
|
|
233
|
-
<FormLabel>
|
|
234
|
-
<Trans>Option Values</Trans>
|
|
235
|
-
</FormLabel>
|
|
236
|
-
<FormControl>
|
|
237
|
-
<OptionValueInput
|
|
238
|
-
groupName={watch(`optionGroups.${index}.name`) || ''}
|
|
239
|
-
groupIndex={index}
|
|
240
|
-
disabled={!watch(`optionGroups.${index}.name`)}
|
|
241
|
-
/>
|
|
242
|
-
</FormControl>
|
|
243
|
-
</FormItem>
|
|
244
|
-
</div>
|
|
245
|
-
|
|
246
|
-
<div className="pt-8">
|
|
247
|
-
<Button
|
|
248
|
-
variant="ghost"
|
|
249
|
-
size="icon"
|
|
250
|
-
onClick={() => removeOptionGroup(index)}
|
|
251
|
-
title="Remove Option"
|
|
252
|
-
>
|
|
253
|
-
<Trash2 className="h-4 w-4" />
|
|
254
|
-
</Button>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
))}
|
|
258
|
-
|
|
259
|
-
<Button type="button" variant="secondary" onClick={handleAddOptionGroup} className="mb-6">
|
|
260
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
261
|
-
<Trans>Add Option</Trans>
|
|
262
|
-
</Button>
|
|
164
|
+
<div className="mb-6">
|
|
165
|
+
<OptionGroupsEditor onChange={data => setOptionGroups(data.optionGroups)} />
|
|
166
|
+
</div>
|
|
263
167
|
|
|
264
168
|
{stockLocations.length === 0 ? (
|
|
265
169
|
<Alert variant="destructive">
|
|
@@ -405,7 +309,7 @@ export function CreateProductVariants({
|
|
|
405
309
|
}
|
|
406
310
|
|
|
407
311
|
// Generate all possible combinations of option values
|
|
408
|
-
function generateVariants(groups:
|
|
312
|
+
function generateVariants(groups: OptionGroupConfiguration['optionGroups']): GeneratedVariant[] {
|
|
409
313
|
// If there are no groups, return a single variant with no options
|
|
410
314
|
if (!groups.length)
|
|
411
315
|
return [
|
|
@@ -427,7 +331,7 @@ function generateVariants(groups: OptionGroupForm[]): GeneratedVariant[] {
|
|
|
427
331
|
|
|
428
332
|
// Generate combinations
|
|
429
333
|
const generateCombinations = (
|
|
430
|
-
optionGroups:
|
|
334
|
+
optionGroups: OptionGroupConfiguration['optionGroups'],
|
|
431
335
|
currentIndex: number,
|
|
432
336
|
currentCombination: VariantOption[],
|
|
433
337
|
): GeneratedVariant[] => {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
2
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import { Form } from '@/vdb/components/ui/form.js';
|
|
4
|
+
import { Input } from '@/vdb/components/ui/input.js';
|
|
5
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
6
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
7
|
+
import { Plus, Trash2 } from 'lucide-react';
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
import { Control, useFieldArray, useForm } from 'react-hook-form';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { OptionValueInput } from './option-value-input.js';
|
|
12
|
+
|
|
13
|
+
export const optionValueSchema = z.object({
|
|
14
|
+
value: z.string().min(1, { message: 'Value cannot be empty' }),
|
|
15
|
+
id: z.string().min(1, { message: 'Value cannot be empty' }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const optionGroupSchema = z.object({
|
|
19
|
+
name: z.string().min(1, { message: 'Option name is required' }),
|
|
20
|
+
values: z.array(optionValueSchema).min(1, { message: 'At least one value is required' }),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const multiGroupFormSchema = z.object({
|
|
24
|
+
optionGroups: z.array(optionGroupSchema),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type OptionGroup = z.infer<typeof optionGroupSchema>;
|
|
28
|
+
export type MultiGroupForm = z.infer<typeof multiGroupFormSchema>;
|
|
29
|
+
|
|
30
|
+
export interface SingleOptionGroup {
|
|
31
|
+
name: string;
|
|
32
|
+
values: Array<{
|
|
33
|
+
value: string;
|
|
34
|
+
id: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface OptionGroupConfiguration {
|
|
39
|
+
optionGroups: SingleOptionGroup[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateOptionGroup(group: any): SingleOptionGroup | null {
|
|
43
|
+
if (!group || typeof group.name !== 'string' || !Array.isArray(group.values)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const validValues = group.values
|
|
48
|
+
.filter((v: any): v is NonNullable<typeof v> => !!v)
|
|
49
|
+
.filter((v: any) => typeof v.value === 'string' && typeof v.id === 'string')
|
|
50
|
+
.map((v: any) => ({
|
|
51
|
+
value: v.value,
|
|
52
|
+
id: v.id,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
return validValues.length > 0 ? { name: group.name, values: validValues } : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SingleOptionGroupEditorProps {
|
|
59
|
+
control: Control<any>;
|
|
60
|
+
fieldArrayPath: string;
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function SingleOptionGroupEditor({
|
|
65
|
+
control,
|
|
66
|
+
fieldArrayPath,
|
|
67
|
+
disabled,
|
|
68
|
+
}: Readonly<SingleOptionGroupEditorProps>) {
|
|
69
|
+
const { fields, append, remove } = useFieldArray({
|
|
70
|
+
control,
|
|
71
|
+
name: [fieldArrayPath, 'values'].join('.'),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="space-y-4">
|
|
76
|
+
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start">
|
|
77
|
+
<div>
|
|
78
|
+
<FormFieldWrapper
|
|
79
|
+
control={control}
|
|
80
|
+
name={[fieldArrayPath, 'name'].join('.')}
|
|
81
|
+
label={<Trans>Option Group Name</Trans>}
|
|
82
|
+
render={({ field }) => <Input placeholder="e.g. Size" {...field} />}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div>
|
|
87
|
+
<FormFieldWrapper
|
|
88
|
+
control={control}
|
|
89
|
+
name="values"
|
|
90
|
+
label={<Trans>Option Values</Trans>}
|
|
91
|
+
render={({ field }) => (
|
|
92
|
+
<OptionValueInput
|
|
93
|
+
fields={fields as any}
|
|
94
|
+
onAdd={append}
|
|
95
|
+
onRemove={remove}
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Multi Option Groups Editor - for use in create product variants
|
|
107
|
+
interface OptionGroupsEditorProps {
|
|
108
|
+
onChange?: (data: OptionGroupConfiguration) => void;
|
|
109
|
+
initialGroups?: OptionGroupConfiguration['optionGroups'];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function OptionGroupsEditor({ onChange, initialGroups = [] }: Readonly<OptionGroupsEditorProps>) {
|
|
113
|
+
const form = useForm<MultiGroupForm>({
|
|
114
|
+
resolver: zodResolver(multiGroupFormSchema),
|
|
115
|
+
defaultValues: {
|
|
116
|
+
optionGroups: initialGroups.length > 0 ? initialGroups : [],
|
|
117
|
+
},
|
|
118
|
+
mode: 'onChange',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { control } = form;
|
|
122
|
+
const {
|
|
123
|
+
fields: optionGroups,
|
|
124
|
+
append: appendOptionGroup,
|
|
125
|
+
remove: removeOptionGroup,
|
|
126
|
+
} = useFieldArray({
|
|
127
|
+
control,
|
|
128
|
+
name: 'optionGroups',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Watch for changes and notify parent
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const subscription = form.watch(value => {
|
|
134
|
+
if (value?.optionGroups) {
|
|
135
|
+
const validOptionGroups = value.optionGroups
|
|
136
|
+
.map(validateOptionGroup)
|
|
137
|
+
.filter((group): group is SingleOptionGroup => group !== null);
|
|
138
|
+
|
|
139
|
+
const filteredData: OptionGroupConfiguration = {
|
|
140
|
+
optionGroups: validOptionGroups,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
onChange?.(filteredData);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return () => subscription.unsubscribe();
|
|
148
|
+
}, [form, onChange]);
|
|
149
|
+
|
|
150
|
+
const handleAddOptionGroup = () => {
|
|
151
|
+
appendOptionGroup({ name: '', values: [] });
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Form {...form}>
|
|
156
|
+
<div className="space-y-4">
|
|
157
|
+
{optionGroups.map((group, index) => (
|
|
158
|
+
<div key={group.id} className="flex items-start">
|
|
159
|
+
<SingleOptionGroupEditor control={control} fieldArrayPath={`optionGroups.${index}`} />
|
|
160
|
+
<div className="shrink-0 mt-6">
|
|
161
|
+
<Button
|
|
162
|
+
variant="ghost"
|
|
163
|
+
size="icon"
|
|
164
|
+
onClick={() => removeOptionGroup(index)}
|
|
165
|
+
title="Remove Option"
|
|
166
|
+
>
|
|
167
|
+
<Trash2 className="h-4 w-4" />
|
|
168
|
+
</Button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
|
|
173
|
+
<Button type="button" variant="secondary" onClick={handleAddOptionGroup}>
|
|
174
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
175
|
+
<Trans>Add Option</Trans>
|
|
176
|
+
</Button>
|
|
177
|
+
</div>
|
|
178
|
+
</Form>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -1,53 +1,32 @@
|
|
|
1
1
|
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
2
2
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
3
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
4
|
-
import {
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import { useFieldArray, useFormContext } from 'react-hook-form';
|
|
7
6
|
|
|
8
7
|
interface OptionValue {
|
|
9
8
|
value: string;
|
|
10
9
|
id: string;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
interface FormValues {
|
|
14
|
-
optionGroups: {
|
|
15
|
-
name: string;
|
|
16
|
-
values: OptionValue[];
|
|
17
|
-
}[];
|
|
18
|
-
variants: Record<
|
|
19
|
-
string,
|
|
20
|
-
{
|
|
21
|
-
enabled: boolean;
|
|
22
|
-
sku: string;
|
|
23
|
-
price: string;
|
|
24
|
-
stock: string;
|
|
25
|
-
}
|
|
26
|
-
>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
12
|
interface OptionValueInputProps {
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
fields: Array<OptionValue>;
|
|
14
|
+
onAdd: (value: OptionValue) => void;
|
|
15
|
+
onRemove: (index: number) => void;
|
|
32
16
|
disabled?: boolean;
|
|
33
17
|
}
|
|
34
18
|
|
|
35
19
|
export function OptionValueInput({
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
fields,
|
|
21
|
+
onAdd,
|
|
22
|
+
onRemove,
|
|
38
23
|
disabled = false,
|
|
39
24
|
}: Readonly<OptionValueInputProps>) {
|
|
40
|
-
const { control } = useFormContext<FormValues>();
|
|
41
|
-
const { fields, append, remove } = useFieldArray({
|
|
42
|
-
control,
|
|
43
|
-
name: `optionGroups.${groupIndex}.values`,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
25
|
const [newValue, setNewValue] = useState('');
|
|
47
26
|
|
|
48
27
|
const handleAddValue = () => {
|
|
49
28
|
if (newValue.trim() && !fields.some(f => f.value === newValue.trim())) {
|
|
50
|
-
|
|
29
|
+
onAdd({ value: newValue.trim(), id: Date.now().toString() });
|
|
51
30
|
setNewValue('');
|
|
52
31
|
}
|
|
53
32
|
};
|
|
@@ -70,15 +49,6 @@ export function OptionValueInput({
|
|
|
70
49
|
disabled={disabled}
|
|
71
50
|
className="flex-1"
|
|
72
51
|
/>
|
|
73
|
-
<Button
|
|
74
|
-
type="button"
|
|
75
|
-
variant="outline"
|
|
76
|
-
size="sm"
|
|
77
|
-
onClick={handleAddValue}
|
|
78
|
-
disabled={disabled || !newValue.trim()}
|
|
79
|
-
>
|
|
80
|
-
<Plus className="h-4 w-4" />
|
|
81
|
-
</Button>
|
|
82
52
|
</div>
|
|
83
53
|
|
|
84
54
|
<div className="flex flex-wrap gap-2">
|
|
@@ -90,7 +60,7 @@ export function OptionValueInput({
|
|
|
90
60
|
variant="ghost"
|
|
91
61
|
size="sm"
|
|
92
62
|
className="h-4 w-4 p-0 ml-1"
|
|
93
|
-
onClick={() =>
|
|
63
|
+
onClick={() => onRemove(index)}
|
|
94
64
|
>
|
|
95
65
|
<X className="h-3 w-3" />
|
|
96
66
|
</Button>
|
|
@@ -55,7 +55,7 @@ export const AssignProductsToChannelBulkAction: BulkActionComponent<any> = ({ se
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
export const RemoveProductsFromChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
58
|
-
const {
|
|
58
|
+
const { activeChannel } = useChannel();
|
|
59
59
|
|
|
60
60
|
return (
|
|
61
61
|
<RemoveFromChannelBulkAction
|
|
@@ -66,7 +66,7 @@ export const RemoveProductsFromChannelBulkAction: BulkActionComponent<any> = ({
|
|
|
66
66
|
requiredPermissions={['UpdateCatalog', 'UpdateProduct']}
|
|
67
67
|
buildInput={() => ({
|
|
68
68
|
productIds: selection.map(s => s.id),
|
|
69
|
-
channelId:
|
|
69
|
+
channelId: activeChannel?.id,
|
|
70
70
|
})}
|
|
71
71
|
/>
|
|
72
72
|
);
|
|
@@ -95,6 +95,46 @@ export const productDetailDocument = graphql(
|
|
|
95
95
|
[productDetailFragment],
|
|
96
96
|
);
|
|
97
97
|
|
|
98
|
+
export const productDetailWithVariantsDocument = graphql(
|
|
99
|
+
`
|
|
100
|
+
query ProductDetailWithVariants($id: ID!) {
|
|
101
|
+
product(id: $id) {
|
|
102
|
+
...ProductDetail
|
|
103
|
+
variantList {
|
|
104
|
+
totalItems
|
|
105
|
+
}
|
|
106
|
+
optionGroups {
|
|
107
|
+
id
|
|
108
|
+
code
|
|
109
|
+
name
|
|
110
|
+
options {
|
|
111
|
+
id
|
|
112
|
+
code
|
|
113
|
+
name
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
variants {
|
|
117
|
+
id
|
|
118
|
+
name
|
|
119
|
+
sku
|
|
120
|
+
price
|
|
121
|
+
currencyCode
|
|
122
|
+
priceWithTax
|
|
123
|
+
createdAt
|
|
124
|
+
updatedAt
|
|
125
|
+
options {
|
|
126
|
+
id
|
|
127
|
+
code
|
|
128
|
+
name
|
|
129
|
+
groupId
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`,
|
|
135
|
+
[productDetailFragment],
|
|
136
|
+
);
|
|
137
|
+
|
|
98
138
|
export const createProductDocument = graphql(`
|
|
99
139
|
mutation CreateProduct($input: CreateProductInput!) {
|
|
100
140
|
createProduct(input: $input) {
|
|
@@ -187,3 +227,99 @@ export const getProductsWithFacetValuesByIdsDocument = graphql(`
|
|
|
187
227
|
}
|
|
188
228
|
}
|
|
189
229
|
`);
|
|
230
|
+
|
|
231
|
+
export const addOptionGroupToProductDocument = graphql(`
|
|
232
|
+
mutation AddOptionGroupToProduct($productId: ID!, $optionGroupId: ID!) {
|
|
233
|
+
addOptionGroupToProduct(productId: $productId, optionGroupId: $optionGroupId) {
|
|
234
|
+
id
|
|
235
|
+
optionGroups {
|
|
236
|
+
id
|
|
237
|
+
code
|
|
238
|
+
name
|
|
239
|
+
options {
|
|
240
|
+
id
|
|
241
|
+
code
|
|
242
|
+
name
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`);
|
|
248
|
+
|
|
249
|
+
export const updateProductVariantDocument = graphql(`
|
|
250
|
+
mutation UpdateProductVariant($input: UpdateProductVariantInput!) {
|
|
251
|
+
updateProductVariant(input: $input) {
|
|
252
|
+
id
|
|
253
|
+
name
|
|
254
|
+
options {
|
|
255
|
+
id
|
|
256
|
+
code
|
|
257
|
+
name
|
|
258
|
+
groupId
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
`);
|
|
263
|
+
|
|
264
|
+
export const deleteProductVariantDocument = graphql(`
|
|
265
|
+
mutation DeleteProductVariant($id: ID!) {
|
|
266
|
+
deleteProductVariant(id: $id) {
|
|
267
|
+
result
|
|
268
|
+
message
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
`);
|
|
272
|
+
|
|
273
|
+
export const removeOptionGroupFromProductDocument = graphql(`
|
|
274
|
+
mutation RemoveOptionGroupFromProduct($productId: ID!, $optionGroupId: ID!) {
|
|
275
|
+
removeOptionGroupFromProduct(productId: $productId, optionGroupId: $optionGroupId) {
|
|
276
|
+
... on Product {
|
|
277
|
+
id
|
|
278
|
+
optionGroups {
|
|
279
|
+
id
|
|
280
|
+
code
|
|
281
|
+
name
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
... on ErrorResult {
|
|
285
|
+
errorCode
|
|
286
|
+
message
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`);
|
|
291
|
+
|
|
292
|
+
export const createProductOptionGroupDocument = graphql(`
|
|
293
|
+
mutation CreateOptionGroups($input: CreateProductOptionGroupInput!) {
|
|
294
|
+
createProductOptionGroup(input: $input) {
|
|
295
|
+
id
|
|
296
|
+
name
|
|
297
|
+
code
|
|
298
|
+
options {
|
|
299
|
+
id
|
|
300
|
+
code
|
|
301
|
+
name
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
`);
|
|
306
|
+
|
|
307
|
+
export const createProductOptionDocument = graphql(`
|
|
308
|
+
mutation CreateProductOption($input: CreateProductOptionInput!) {
|
|
309
|
+
createProductOption(input: $input) {
|
|
310
|
+
id
|
|
311
|
+
code
|
|
312
|
+
name
|
|
313
|
+
groupId
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
`);
|
|
317
|
+
|
|
318
|
+
export const createProductVariantsDocument = graphql(`
|
|
319
|
+
mutation CreateProductVariants($input: [CreateProductVariantInput!]!) {
|
|
320
|
+
createProductVariants(input: $input) {
|
|
321
|
+
id
|
|
322
|
+
name
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
`);
|