@vendure/dashboard 3.3.2 → 3.3.3

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 (37) hide show
  1. package/dist/plugin/utils/config-loader.d.ts +12 -1
  2. package/dist/plugin/utils/config-loader.js +25 -7
  3. package/dist/plugin/vite-plugin-vendure-dashboard.d.ts +8 -0
  4. package/dist/plugin/vite-plugin-vendure-dashboard.js +5 -1
  5. package/package.json +4 -4
  6. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +1 -4
  7. package/src/app/routes/_authenticated/_channels/channels.tsx +18 -0
  8. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +1 -5
  9. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -4
  10. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +1 -4
  11. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +1 -4
  12. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -5
  13. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +56 -74
  14. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +369 -0
  15. package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +435 -0
  16. package/src/app/routes/_authenticated/_products/components/product-option-select.tsx +117 -0
  17. package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +4 -2
  18. package/src/app/routes/_authenticated/_products/products_.$id.tsx +17 -3
  19. package/src/app/routes/_authenticated/_profile/profile.tsx +1 -4
  20. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -4
  21. package/src/lib/components/data-table/data-table-view-options.tsx +12 -2
  22. package/src/lib/components/data-table/data-table.tsx +9 -0
  23. package/src/lib/components/layout/channel-switcher.tsx +1 -2
  24. package/src/lib/components/shared/assigned-facet-values.tsx +13 -14
  25. package/src/lib/components/shared/entity-assets.tsx +140 -70
  26. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -0
  27. package/src/lib/components/ui/button.tsx +1 -1
  28. package/src/lib/framework/form-engine/use-generated-form.tsx +1 -0
  29. package/src/lib/framework/page/list-page.tsx +2 -2
  30. package/src/lib/framework/page/use-detail-page.ts +7 -0
  31. package/src/lib/graphql/api.ts +10 -1
  32. package/src/lib/hooks/use-permissions.ts +4 -4
  33. package/src/lib/providers/auth.tsx +9 -3
  34. package/src/lib/providers/channel-provider.tsx +64 -24
  35. package/src/lib/providers/server-config.tsx +2 -2
  36. package/vite/utils/config-loader.ts +48 -13
  37. package/vite/vite-plugin-vendure-dashboard.ts +14 -4
@@ -0,0 +1,435 @@
1
+ import { Button } from '@/components/ui/button.js';
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogFooter,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ DialogTrigger,
9
+ } from '@/components/ui/dialog.js';
10
+ import { Form } from '@/components/ui/form.js';
11
+ import { Input } from '@/components/ui/input.js';
12
+ import { FormFieldWrapper } from '@/components/shared/form-field-wrapper.js';
13
+ import { api } from '@/graphql/api.js';
14
+ import { graphql } from '@/graphql/graphql.js';
15
+ import { Trans, useLingui } from '@/lib/trans.js';
16
+ import { zodResolver } from '@hookform/resolvers/zod';
17
+ import { useMutation, useQuery } from '@tanstack/react-query';
18
+ import { Plus, Trash2 } from 'lucide-react';
19
+ import { useState } from 'react';
20
+ import { useForm } from 'react-hook-form';
21
+ import { toast } from 'sonner';
22
+ import * as z from 'zod';
23
+
24
+ const getProductDocument = graphql(`
25
+ query GetProduct($productId: ID!) {
26
+ product(id: $productId) {
27
+ id
28
+ name
29
+ variants {
30
+ id
31
+ name
32
+ sku
33
+ options {
34
+ id
35
+ code
36
+ name
37
+ groupId
38
+ }
39
+ }
40
+ optionGroups {
41
+ id
42
+ code
43
+ name
44
+ options {
45
+ id
46
+ code
47
+ name
48
+ }
49
+ }
50
+ }
51
+ }
52
+ `);
53
+
54
+ const createProductOptionGroupDocument = graphql(`
55
+ mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
56
+ createProductOptionGroup(input: $input) {
57
+ id
58
+ code
59
+ name
60
+ options {
61
+ id
62
+ code
63
+ name
64
+ }
65
+ }
66
+ }
67
+ `);
68
+
69
+ const addOptionGroupToProductDocument = graphql(`
70
+ mutation AddOptionGroupToProduct($productId: ID!, $optionGroupId: ID!) {
71
+ addOptionGroupToProduct(productId: $productId, optionGroupId: $optionGroupId) {
72
+ id
73
+ optionGroups {
74
+ id
75
+ code
76
+ name
77
+ options {
78
+ id
79
+ code
80
+ name
81
+ }
82
+ }
83
+ }
84
+ }
85
+ `);
86
+
87
+ const updateProductVariantDocument = graphql(`
88
+ mutation UpdateProductVariant($input: UpdateProductVariantInput!) {
89
+ updateProductVariant(input: $input) {
90
+ id
91
+ name
92
+ options {
93
+ id
94
+ code
95
+ name
96
+ }
97
+ }
98
+ }
99
+ `);
100
+
101
+ const formSchema = z.object({
102
+ optionGroups: z.array(z.object({
103
+ name: z.string().min(1, 'Option group name is required'),
104
+ options: z.array(z.string().min(1, 'Option name is required')).min(1, 'At least one option is required'),
105
+ })).min(1, 'At least one option group is required'),
106
+ existingVariantOptionIds: z.array(z.string()).min(1, 'Must select an option for the existing variant'),
107
+ });
108
+
109
+ type FormValues = z.infer<typeof formSchema>;
110
+
111
+ export function CreateProductOptionsDialog({
112
+ productId,
113
+ onSuccess,
114
+ }: {
115
+ productId: string;
116
+ onSuccess?: () => void;
117
+ }) {
118
+ const [open, setOpen] = useState(false);
119
+ const { i18n } = useLingui();
120
+
121
+ const { data: productData } = useQuery({
122
+ queryKey: ['product', productId],
123
+ queryFn: () => api.query(getProductDocument, { productId }),
124
+ });
125
+
126
+ const form = useForm<FormValues>({
127
+ resolver: zodResolver(formSchema),
128
+ defaultValues: {
129
+ optionGroups: [{ name: '', options: [''] }],
130
+ existingVariantOptionIds: [],
131
+ },
132
+ });
133
+
134
+ const createProductOptionGroupMutation = useMutation({
135
+ mutationFn: api.mutate(createProductOptionGroupDocument),
136
+ });
137
+
138
+ const addOptionGroupToProductMutation = useMutation({
139
+ mutationFn: api.mutate(addOptionGroupToProductDocument),
140
+ });
141
+
142
+ const updateProductVariantMutation = useMutation({
143
+ mutationFn: api.mutate(updateProductVariantDocument),
144
+ onSuccess: () => {
145
+ toast.success(i18n.t('Successfully created product options'));
146
+ setOpen(false);
147
+ onSuccess?.();
148
+ },
149
+ onError: (error) => {
150
+ toast.error(i18n.t('Failed to create product options'), {
151
+ description: error instanceof Error ? error.message : i18n.t('Unknown error'),
152
+ });
153
+ }
154
+ });
155
+
156
+ const onSubmit = async (values: FormValues) => {
157
+ if (!productData?.product) return;
158
+
159
+ try {
160
+ // Create all option groups and their options
161
+ const createdOptionGroups = await Promise.all(
162
+ values.optionGroups.map(async (group) => {
163
+ const result = await createProductOptionGroupMutation.mutateAsync({
164
+ input: {
165
+ code: group.name.toLowerCase().replace(/\s+/g, '-'),
166
+ translations: [
167
+ {
168
+ languageCode: "en",
169
+ name: group.name,
170
+ },
171
+ ],
172
+ options: group.options.map(option => ({
173
+ code: option.toLowerCase().replace(/\s+/g, '-'),
174
+ translations: [
175
+ {
176
+ languageCode: "en",
177
+ name: option,
178
+ },
179
+ ],
180
+ })),
181
+ },
182
+ });
183
+
184
+ // Add the option group to the product
185
+ await addOptionGroupToProductMutation.mutateAsync({
186
+ productId,
187
+ optionGroupId: result.createProductOptionGroup.id,
188
+ });
189
+
190
+ return result.createProductOptionGroup;
191
+ })
192
+ );
193
+
194
+ // Combine existing and newly created option groups
195
+ const allOptionGroups = [
196
+ ...(productData.product.optionGroups || []),
197
+ ...createdOptionGroups,
198
+ ];
199
+
200
+ // Map the selected option names to their IDs
201
+ const selectedOptionIds = values.existingVariantOptionIds.map((optionName, index) => {
202
+ const group = allOptionGroups[index];
203
+ const option = group.options.find(opt => opt.name === optionName);
204
+ if (!option) {
205
+ throw new Error(`Option "${optionName}" not found in group "${group.name}"`);
206
+ }
207
+ return option.id;
208
+ });
209
+
210
+ // Update the existing variant with the selected options
211
+ if (productData.product.variants[0]) {
212
+ // Create a new name by appending the selected option names
213
+ const selectedOptionNames = values.existingVariantOptionIds
214
+ .map((optionName, index) => {
215
+ const group = allOptionGroups[index];
216
+ const option = group.options.find(opt => opt.name === optionName);
217
+ return option?.name;
218
+ })
219
+ .filter(Boolean)
220
+ .join(' ');
221
+
222
+ const newVariantName = `${productData.product.name} ${selectedOptionNames}`;
223
+
224
+ await updateProductVariantMutation.mutateAsync({
225
+ input: {
226
+ id: productData.product.variants[0].id,
227
+ optionIds: selectedOptionIds,
228
+ translations: [
229
+ {
230
+ languageCode: "en",
231
+ name: newVariantName,
232
+ },
233
+ ],
234
+ },
235
+ });
236
+ }
237
+ } catch (error) {
238
+ toast.error(i18n.t('Failed to create product options'), {
239
+ description: error instanceof Error ? error.message : i18n.t('Unknown error'),
240
+ });
241
+ }
242
+ };
243
+
244
+ const addOptionGroup = () => {
245
+ const currentGroups = form.getValues('optionGroups');
246
+ form.setValue('optionGroups', [...currentGroups, { name: '', options: [''] }]);
247
+ };
248
+
249
+ const removeOptionGroup = (index: number) => {
250
+ const currentGroups = form.getValues('optionGroups');
251
+ form.setValue('optionGroups', currentGroups.filter((_, i) => i !== index));
252
+ };
253
+
254
+ const addOption = (groupIndex: number) => {
255
+ const currentGroups = form.getValues('optionGroups');
256
+ const updatedGroups = [...currentGroups];
257
+ updatedGroups[groupIndex].options.push('');
258
+ form.setValue('optionGroups', updatedGroups);
259
+ };
260
+
261
+ const removeOption = (groupIndex: number, optionIndex: number) => {
262
+ const currentGroups = form.getValues('optionGroups');
263
+ const updatedGroups = [...currentGroups];
264
+ updatedGroups[groupIndex].options = updatedGroups[groupIndex].options.filter((_, i) => i !== optionIndex);
265
+ form.setValue('optionGroups', updatedGroups);
266
+ };
267
+
268
+ return (
269
+ <Dialog open={open} onOpenChange={setOpen}>
270
+ <DialogTrigger asChild>
271
+ <Button variant="outline">
272
+ <Plus className="mr-2 h-4 w-4" />
273
+ <Trans>Create options</Trans>
274
+ </Button>
275
+ </DialogTrigger>
276
+ <DialogContent className="max-w-2xl">
277
+ <DialogHeader>
278
+ <DialogTitle>
279
+ <Trans>Create product options</Trans>
280
+ </DialogTitle>
281
+ </DialogHeader>
282
+ <Form {...form}>
283
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
284
+ <div className="space-y-4">
285
+ {form.watch('optionGroups').map((group, groupIndex) => (
286
+ <div key={groupIndex} className="space-y-4 p-4 border rounded-lg">
287
+ <div className="flex justify-between items-center">
288
+ <FormFieldWrapper
289
+ control={form.control}
290
+ name={`optionGroups.${groupIndex}.name`}
291
+ label={<Trans>Option group name</Trans>}
292
+ render={({ field }) => (
293
+ <Input {...field} placeholder={i18n.t('e.g. Size')} />
294
+ )}
295
+ />
296
+ {groupIndex > 0 && (
297
+ <Button
298
+ type="button"
299
+ variant="ghost"
300
+ onClick={() => removeOptionGroup(groupIndex)}
301
+ >
302
+ <Trans>Remove group</Trans>
303
+ </Button>
304
+ )}
305
+ </div>
306
+ <div className="space-y-2">
307
+ {group.options.map((_, optionIndex) => (
308
+ <div key={optionIndex} className="flex gap-2 items-end">
309
+ <FormFieldWrapper
310
+ control={form.control}
311
+ name={`optionGroups.${groupIndex}.options.${optionIndex}`}
312
+ label={<Trans>Option name</Trans>}
313
+ render={({ field }) => (
314
+ <Input {...field} placeholder={i18n.t('e.g. Small')} />
315
+ )}
316
+ />
317
+ {optionIndex > 0 && (
318
+ <Button
319
+ type="button"
320
+ variant="ghost"
321
+ size="icon"
322
+ onClick={() => removeOption(groupIndex, optionIndex)}
323
+ >
324
+ <Trash2 className="h-4 w-4" />
325
+ </Button>
326
+ )}
327
+ </div>
328
+ ))}
329
+ <Button
330
+ type="button"
331
+ variant="outline"
332
+ onClick={() => addOption(groupIndex)}
333
+ >
334
+ <Plus className="mr-2 h-4 w-4" />
335
+ <Trans>Add option</Trans>
336
+ </Button>
337
+ </div>
338
+ </div>
339
+ ))}
340
+ <Button
341
+ type="button"
342
+ variant="outline"
343
+ size="sm"
344
+ onClick={addOptionGroup}
345
+ >
346
+ <Plus className="mr-2 h-4 w-4" />
347
+ <Trans>Add another option group</Trans>
348
+ </Button>
349
+ </div>
350
+
351
+ {productData?.product?.variants[0] && (
352
+ <div className="space-y-4 p-4 border rounded-lg">
353
+ <h3 className="font-medium">
354
+ <Trans>Assign options to existing variant</Trans>
355
+ </h3>
356
+ <p className="text-sm text-muted-foreground">
357
+ <Trans>Select which options should apply to the existing variant "{productData.product.variants[0].name}"</Trans>
358
+ </p>
359
+ {/* Show existing option groups first */}
360
+ {productData.product.optionGroups?.map((group, groupIndex) => (
361
+ <FormFieldWrapper
362
+ key={group.id}
363
+ control={form.control}
364
+ name={`existingVariantOptionIds.${groupIndex}`}
365
+ label={group.name}
366
+ render={({ field }) => (
367
+ <select
368
+ className="w-full p-2 border rounded-md"
369
+ value={field.value}
370
+ onChange={(e) => {
371
+ const newValues = [...form.getValues('existingVariantOptionIds')];
372
+ newValues[groupIndex] = e.target.value;
373
+ form.setValue('existingVariantOptionIds', newValues);
374
+ }}
375
+ >
376
+ <option value="">Select an option</option>
377
+ {group.options.map((option) => (
378
+ <option key={option.id} value={option.name}>
379
+ {option.name}
380
+ </option>
381
+ ))}
382
+ </select>
383
+ )}
384
+ />
385
+ ))}
386
+ {/* Then show new option groups */}
387
+ {form.watch('optionGroups').map((group, groupIndex) => (
388
+ <FormFieldWrapper
389
+ key={`new-${groupIndex}`}
390
+ control={form.control}
391
+ name={`existingVariantOptionIds.${(productData?.product?.optionGroups?.length || 0) + groupIndex}`}
392
+ label={group.name || <Trans>Option group {groupIndex + 1}</Trans>}
393
+ render={({ field }) => (
394
+ <select
395
+ className="w-full p-2 border rounded-md"
396
+ value={field.value}
397
+ onChange={(e) => {
398
+ const newValues = [...form.getValues('existingVariantOptionIds')];
399
+ newValues[(productData?.product?.optionGroups?.length || 0) + groupIndex] = e.target.value;
400
+ form.setValue('existingVariantOptionIds', newValues);
401
+ }}
402
+ >
403
+ <option value="">Select an option</option>
404
+ {group.options.map((option, optionIndex) => (
405
+ <option key={optionIndex} value={option}>
406
+ {option}
407
+ </option>
408
+ ))}
409
+ </select>
410
+ )}
411
+ />
412
+ ))}
413
+ </div>
414
+ )}
415
+
416
+ <DialogFooter>
417
+ <Button
418
+ type="submit"
419
+ disabled={
420
+ createProductOptionGroupMutation.isPending ||
421
+ addOptionGroupToProductMutation.isPending ||
422
+ updateProductVariantMutation.isPending ||
423
+ (productData?.product?.variants[0] &&
424
+ form.watch('existingVariantOptionIds').some(value => !value))
425
+ }
426
+ >
427
+ <Trans>Create options</Trans>
428
+ </Button>
429
+ </DialogFooter>
430
+ </form>
431
+ </Form>
432
+ </DialogContent>
433
+ </Dialog>
434
+ );
435
+ }
@@ -0,0 +1,117 @@
1
+ import { useState } from 'react';
2
+ import { Plus, Check, ChevronsUpDown } from 'lucide-react';
3
+ import { Button } from '@/components/ui/button.js';
4
+ import { FormFieldWrapper } from '@/components/shared/form-field-wrapper.js';
5
+ import {
6
+ Command,
7
+ CommandEmpty,
8
+ CommandGroup,
9
+ CommandInput,
10
+ CommandItem,
11
+ } from '@/components/ui/command.js';
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from '@/components/ui/popover.js';
17
+ import { cn } from '@/lib/utils.js';
18
+ import { Trans, useLingui } from '@/lib/trans.js';
19
+
20
+ interface ProductOption {
21
+ id: string;
22
+ code: string;
23
+ name: string;
24
+ }
25
+
26
+ interface ProductOptionGroup {
27
+ id: string;
28
+ code: string;
29
+ name: string;
30
+ options: ProductOption[];
31
+ }
32
+
33
+ interface ProductOptionSelectProps {
34
+ group: ProductOptionGroup;
35
+ value: string;
36
+ onChange: (value: string) => void;
37
+ onCreateOption: (name: string) => void;
38
+ }
39
+
40
+ export function ProductOptionSelect({ group, value, onChange, onCreateOption }: ProductOptionSelectProps) {
41
+ const [open, setOpen] = useState(false);
42
+ const [newOptionInput, setNewOptionInput] = useState('');
43
+ const { i18n } = useLingui();
44
+
45
+ return (
46
+ <FormFieldWrapper
47
+ control={undefined}
48
+ name={`options.${group.id}`}
49
+ label={group.name}
50
+ render={() => (
51
+ <Popover
52
+ open={open}
53
+ onOpenChange={setOpen}
54
+ >
55
+ <PopoverTrigger asChild>
56
+ <Button
57
+ variant="outline"
58
+ role="combobox"
59
+ className="w-full justify-between"
60
+ >
61
+ {value
62
+ ? group.options.find(
63
+ (option) => option.id === value,
64
+ )?.name
65
+ : <Trans>Select {group.name}</Trans>}
66
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
67
+ </Button>
68
+ </PopoverTrigger>
69
+ <PopoverContent className="w-full p-0">
70
+ <Command>
71
+ <CommandInput
72
+ placeholder={i18n.t('Search {name}...').replace('{name}', group.name)}
73
+ onValueChange={setNewOptionInput}
74
+ />
75
+ <CommandEmpty className='py-2'>
76
+ <div className="p-2">
77
+ <Button
78
+ variant="outline"
79
+ className="w-full justify-start"
80
+ onClick={() => {
81
+ if (newOptionInput) {
82
+ onCreateOption(newOptionInput);
83
+ }
84
+ }}
85
+ >
86
+ <Plus className="mr-2 h-4 w-4" />
87
+ <Trans>Add "{newOptionInput}"</Trans>
88
+ </Button>
89
+ </div>
90
+ </CommandEmpty>
91
+ <CommandGroup>
92
+ {group.options.map((option) => (
93
+ <CommandItem
94
+ key={option.id}
95
+ value={option.name}
96
+ onSelect={() => {
97
+ onChange(option.id);
98
+ setOpen(false);
99
+ }}
100
+ >
101
+ <Check
102
+ className={cn(
103
+ "mr-2 h-4 w-4",
104
+ value === option.id ? "opacity-100" : "opacity-0"
105
+ )}
106
+ />
107
+ {option.name}
108
+ </CommandItem>
109
+ ))}
110
+ </CommandGroup>
111
+ </Command>
112
+ </PopoverContent>
113
+ </Popover>
114
+ )}
115
+ />
116
+ );
117
+ }
@@ -1,4 +1,4 @@
1
- import { PaginatedListDataTable } from "@/components/shared/paginated-list-data-table.js";
1
+ import { PaginatedListDataTable, PaginatedListRefresherRegisterFn } from "@/components/shared/paginated-list-data-table.js";
2
2
  import { productVariantListDocument } from "../products.graphql.js";
3
3
  import { useState } from "react";
4
4
  import { ColumnFiltersState, SortingState } from "@tanstack/react-table";
@@ -9,9 +9,10 @@ import { Button } from "@/components/ui/button.js";
9
9
 
10
10
  interface ProductVariantsTableProps {
11
11
  productId: string;
12
+ registerRefresher?: PaginatedListRefresherRegisterFn;
12
13
  }
13
14
 
14
- export function ProductVariantsTable({ productId }: ProductVariantsTableProps) {
15
+ export function ProductVariantsTable({ productId, registerRefresher }: ProductVariantsTableProps) {
15
16
  const { formatCurrencyName } = useLocalFormat();
16
17
  const [page, setPage] = useState(1);
17
18
  const [pageSize, setPageSize] = useState(10);
@@ -19,6 +20,7 @@ export function ProductVariantsTable({ productId }: ProductVariantsTableProps) {
19
20
  const [filters, setFilters] = useState<ColumnFiltersState>([]);
20
21
 
21
22
  return <PaginatedListDataTable
23
+ registerRefresher={registerRefresher}
22
24
  listQuery={productVariantListDocument}
23
25
  transformVariables={variables => ({
24
26
  ...variables,
@@ -27,7 +27,9 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
27
27
  import { toast } from 'sonner';
28
28
  import { CreateProductVariantsDialog } from './components/create-product-variants-dialog.js';
29
29
  import { ProductVariantsTable } from './components/product-variants-table.js';
30
+ import { AddProductVariantDialog } from './components/add-product-variant-dialog.js';
30
31
  import { createProductDocument, productDetailDocument, updateProductDocument } from './products.graphql.js';
32
+ import { useRef } from 'react';
31
33
 
32
34
  export const Route = createFileRoute('/_authenticated/_products/products_/$id')({
33
35
  component: ProductDetailPage,
@@ -48,6 +50,7 @@ function ProductDetailPage() {
48
50
  const navigate = useNavigate();
49
51
  const creatingNewEntity = params.id === NEW_ENTITY_PATH;
50
52
  const { i18n } = useLingui();
53
+ const refreshRef = useRef<() => void>(() => {});
51
54
 
52
55
  const { form, submitHandler, entity, isPending, refreshEntity, resetForm } = useDetailPage({
53
56
  queryDocument: productDetailDocument,
@@ -68,7 +71,7 @@ function ProductDetailPage() {
68
71
  description: translation.description,
69
72
  customFields: (translation as any).customFields,
70
73
  })),
71
- customFields: entity.customFields,
74
+ customFields: entity.customFields as any,
72
75
  };
73
76
  },
74
77
  params: { id: params.id },
@@ -85,7 +88,7 @@ function ProductDetailPage() {
85
88
  });
86
89
  },
87
90
  });
88
-
91
+
89
92
  return (
90
93
  <Page pageId="product-detail" entity={entity} form={form} submitHandler={submitHandler}>
91
94
  <PageTitle>{creatingNewEntity ? <Trans>New product</Trans> : (entity?.name ?? '')}</PageTitle>
@@ -139,7 +142,18 @@ function ProductDetailPage() {
139
142
  <CustomFieldsPageBlock column="main" entityType="Product" control={form.control} />
140
143
  {entity && entity.variantList.totalItems > 0 && (
141
144
  <PageBlock column="main" blockId="product-variants-table">
142
- <ProductVariantsTable productId={params.id} />
145
+ <ProductVariantsTable productId={params.id} registerRefresher={refresher => {
146
+ refreshRef.current = refresher;
147
+ }} />
148
+ <div className="mt-4">
149
+ <AddProductVariantDialog
150
+ productId={params.id}
151
+ onSuccess={() => {
152
+ console.log('onSuccess');
153
+ refreshRef.current?.();
154
+ }}
155
+ />
156
+ </div>
143
157
  </PageBlock>
144
158
  )}
145
159
  {entity && entity.variantList.totalItems === 0 && (
@@ -58,14 +58,11 @@ function ProfilePage() {
58
58
  },
59
59
  params: { id: 'undefined' },
60
60
  onSuccess: async data => {
61
- toast(i18n.t('Successfully updated profile'), {
62
- position: 'top-right',
63
- });
61
+ toast(i18n.t('Successfully updated profile'));
64
62
  form.reset(form.getValues());
65
63
  },
66
64
  onError: err => {
67
65
  toast(i18n.t('Failed to update profile'), {
68
- position: 'top-right',
69
66
  description: err instanceof Error ? err.message : 'Unknown error',
70
67
  });
71
68
  },
@@ -51,9 +51,7 @@ function SellerDetailPage() {
51
51
  },
52
52
  params: { id: params.id },
53
53
  onSuccess: async data => {
54
- toast(i18n.t('Successfully updated seller'), {
55
- position: 'top-right',
56
- });
54
+ toast(i18n.t('Successfully updated seller'));
57
55
  form.reset(form.getValues());
58
56
  if (creatingNewEntity) {
59
57
  await navigate({ to: `../${data?.id}`, from: Route.id });
@@ -61,7 +59,6 @@ function SellerDetailPage() {
61
59
  },
62
60
  onError: err => {
63
61
  toast(i18n.t('Failed to update seller'), {
64
- position: 'top-right',
65
62
  description: err instanceof Error ? err.message : 'Unknown error',
66
63
  });
67
64
  },