@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.
Files changed (31) hide show
  1. package/package.json +4 -4
  2. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +3 -0
  3. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +7 -7
  4. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +3 -3
  5. package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +2 -2
  6. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -4
  7. package/src/app/routes/_authenticated/_products/components/add-option-group-dialog.tsx +127 -0
  8. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +41 -39
  9. package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +1 -33
  10. package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +7 -42
  11. package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +38 -134
  12. package/src/app/routes/_authenticated/_products/components/option-groups-editor.tsx +180 -0
  13. package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +9 -39
  14. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +2 -2
  15. package/src/app/routes/_authenticated/_products/products.graphql.ts +136 -0
  16. package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -9
  17. package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +405 -0
  18. package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +2 -2
  19. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +2 -2
  20. package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +3 -3
  21. package/src/lib/components/data-input/rich-text-input.tsx +8 -4
  22. package/src/lib/components/layout/channel-switcher.tsx +27 -6
  23. package/src/lib/components/layout/manage-languages-dialog.tsx +2 -2
  24. package/src/lib/components/shared/asset/asset-gallery.tsx +20 -2
  25. package/src/lib/components/shared/asset/asset-picker-dialog.tsx +5 -5
  26. package/src/lib/components/shared/assign-to-channel-dialog.tsx +2 -2
  27. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -2
  28. package/src/lib/graphql/api.ts +3 -1
  29. package/src/lib/hooks/use-permissions.ts +4 -4
  30. package/src/lib/providers/auth.tsx +8 -0
  31. 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 { Plus, Trash2 } from 'lucide-react';
13
- import { useEffect, useMemo } from 'react';
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 { OptionValueInput } from './option-value-input.js';
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 form = useForm<FormValues>({
111
- resolver: zodResolver(formSchema),
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 { control, watch, setValue } = form;
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((value, { name, type }) => {
139
- if (value?.optionGroups) {
140
- const formVariants = value.variants || {};
141
- const activeVariants: VariantConfiguration['variants'] = [];
142
-
143
- variants.forEach(variant => {
144
- if (variant && typeof variant === 'object') {
145
- const formVariant = formVariants[variant.id];
146
- if (formVariant) {
147
- activeVariants.push({
148
- enabled: formVariant.enabled ?? true,
149
- sku: formVariant.sku ?? '',
150
- price: formVariant.price ?? '',
151
- stock: formVariant.stock ?? '',
152
- options: variant.options,
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
- onChange?.({ data: filteredData });
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
- {optionGroups.map((group, index) => (
212
- <div key={group.id} className="grid grid-cols-[1fr_2fr_auto] gap-4 mb-6 items-start">
213
- <div>
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: OptionGroupForm[]): GeneratedVariant[] {
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: OptionGroupForm[],
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 { Plus, X } from 'lucide-react';
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
- groupName: string;
31
- groupIndex: number;
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
- groupName,
37
- groupIndex,
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
- append({ value: newValue.trim(), id: Date.now().toString() });
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={() => remove(index)}
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 { selectedChannel } = useChannel();
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: selectedChannel?.id,
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
+ `);