@vendure/dashboard 3.4.3-master-202509260228 → 3.5.0-minor-202509261210

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 (74) hide show
  1. package/dist/plugin/api/api-extensions.js +11 -14
  2. package/dist/plugin/api/metrics.resolver.d.ts +2 -2
  3. package/dist/plugin/api/metrics.resolver.js +2 -2
  4. package/dist/plugin/config/metrics-strategies.d.ts +9 -9
  5. package/dist/plugin/config/metrics-strategies.js +6 -6
  6. package/dist/plugin/constants.d.ts +2 -0
  7. package/dist/plugin/constants.js +3 -1
  8. package/dist/plugin/dashboard.plugin.js +13 -0
  9. package/dist/plugin/service/metrics.service.d.ts +3 -3
  10. package/dist/plugin/service/metrics.service.js +37 -53
  11. package/dist/plugin/types.d.ts +9 -12
  12. package/dist/plugin/types.js +7 -11
  13. package/dist/vite/vite-plugin-vendure-dashboard.js +2 -2
  14. package/package.json +4 -4
  15. package/src/app/routes/_authenticated/_collections/collections.tsx +7 -2
  16. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +15 -2
  17. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +14 -2
  18. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +10 -0
  19. package/src/app/routes/_authenticated/_products/components/product-option-group-badge.tsx +19 -0
  20. package/src/app/routes/_authenticated/_products/components/product-options-table.tsx +111 -0
  21. package/src/app/routes/_authenticated/_products/product-option-groups.graphql.ts +103 -0
  22. package/src/app/routes/_authenticated/_products/products.graphql.ts +13 -1
  23. package/src/app/routes/_authenticated/_products/products.tsx +27 -3
  24. package/src/app/routes/_authenticated/_products/products_.$id.tsx +26 -9
  25. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +181 -0
  26. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +208 -0
  27. package/src/app/routes/_authenticated/_zones/components/zone-countries-sheet.tsx +4 -1
  28. package/src/app/routes/_authenticated/index.tsx +41 -24
  29. package/src/lib/components/data-display/json.tsx +16 -1
  30. package/src/lib/components/data-input/index.ts +3 -0
  31. package/src/lib/components/data-input/slug-input.tsx +296 -0
  32. package/src/lib/components/data-table/add-filter-menu.tsx +13 -6
  33. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +38 -1
  34. package/src/lib/components/data-table/data-table-context.tsx +91 -0
  35. package/src/lib/components/data-table/data-table-filter-badge.tsx +9 -5
  36. package/src/lib/components/data-table/data-table-view-options.tsx +17 -8
  37. package/src/lib/components/data-table/data-table.tsx +146 -94
  38. package/src/lib/components/data-table/global-views-bar.tsx +97 -0
  39. package/src/lib/components/data-table/global-views-sheet.tsx +11 -0
  40. package/src/lib/components/data-table/manage-global-views-button.tsx +26 -0
  41. package/src/lib/components/data-table/my-views-button.tsx +47 -0
  42. package/src/lib/components/data-table/refresh-button.tsx +12 -3
  43. package/src/lib/components/data-table/save-view-button.tsx +45 -0
  44. package/src/lib/components/data-table/save-view-dialog.tsx +113 -0
  45. package/src/lib/components/data-table/use-generated-columns.tsx +3 -1
  46. package/src/lib/components/data-table/user-views-sheet.tsx +11 -0
  47. package/src/lib/components/data-table/views-sheet.tsx +297 -0
  48. package/src/lib/components/date-range-picker.tsx +184 -0
  49. package/src/lib/components/shared/paginated-list-data-table.tsx +59 -32
  50. package/src/lib/components/ui/button.tsx +1 -1
  51. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +29 -2
  52. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +10 -7
  53. package/src/lib/framework/dashboard-widget/metrics-widget/metrics-widget.graphql.ts +9 -3
  54. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +19 -75
  55. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +33 -0
  56. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +319 -9
  57. package/src/lib/framework/document-introspection/add-custom-fields.ts +60 -31
  58. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +1 -159
  59. package/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts +1840 -0
  60. package/src/lib/framework/document-introspection/include-only-selected-list-fields.ts +940 -0
  61. package/src/lib/framework/document-introspection/testing-utils.ts +161 -0
  62. package/src/lib/framework/extension-api/display-component-extensions.tsx +2 -0
  63. package/src/lib/framework/extension-api/types/data-table.ts +62 -4
  64. package/src/lib/framework/extension-api/types/navigation.ts +16 -0
  65. package/src/lib/framework/form-engine/utils.ts +34 -0
  66. package/src/lib/framework/page/list-page.tsx +289 -4
  67. package/src/lib/framework/page/use-extended-router.tsx +59 -17
  68. package/src/lib/graphql/api.ts +4 -2
  69. package/src/lib/graphql/graphql-env.d.ts +13 -10
  70. package/src/lib/hooks/use-extended-list-query.ts +5 -0
  71. package/src/lib/hooks/use-saved-views.ts +230 -0
  72. package/src/lib/index.ts +15 -0
  73. package/src/lib/types/saved-views.ts +39 -0
  74. package/src/lib/utils/saved-views-utils.ts +40 -0
@@ -0,0 +1,111 @@
1
+ import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
2
+ import { PaginatedListDataTable } from '@/vdb/components/shared/paginated-list-data-table.js';
3
+ import { Button } from '@/vdb/components/ui/button.js';
4
+ import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
5
+ import { graphql } from '@/vdb/graphql/graphql.js';
6
+ import { Trans } from '@/vdb/lib/trans.js';
7
+ import { Link } from '@tanstack/react-router';
8
+ import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
9
+ import { PlusIcon } from 'lucide-react';
10
+ import { useRef, useState } from 'react';
11
+ import { deleteProductOptionDocument } from '../product-option-groups.graphql.js';
12
+
13
+ export const productOptionListDocument = graphql(`
14
+ query ProductOptionList($options: ProductOptionListOptions, $groupId: ID) {
15
+ productOptions(options: $options, groupId: $groupId) {
16
+ items {
17
+ id
18
+ createdAt
19
+ updatedAt
20
+ name
21
+ code
22
+ customFields
23
+ }
24
+ totalItems
25
+ }
26
+ }
27
+ `);
28
+
29
+ export interface ProductOptionsTableProps {
30
+ productOptionGroupId: string;
31
+ registerRefresher?: (refresher: () => void) => void;
32
+ }
33
+
34
+ export function ProductOptionsTable({
35
+ productOptionGroupId,
36
+ registerRefresher,
37
+ }: Readonly<ProductOptionsTableProps>) {
38
+ const [sorting, setSorting] = useState<SortingState>([]);
39
+ const [page, setPage] = useState(1);
40
+ const [pageSize, setPageSize] = useState(10);
41
+ const [filters, setFilters] = useState<ColumnFiltersState>([]);
42
+ const refreshRef = useRef<() => void>(() => {});
43
+
44
+ return (
45
+ <>
46
+ <PaginatedListDataTable
47
+ listQuery={addCustomFields(productOptionListDocument)}
48
+ deleteMutation={deleteProductOptionDocument}
49
+ page={page}
50
+ itemsPerPage={pageSize}
51
+ sorting={sorting}
52
+ columnFilters={filters}
53
+ onPageChange={(_, page, perPage) => {
54
+ setPage(page);
55
+ setPageSize(perPage);
56
+ }}
57
+ onSortChange={(_, sorting) => {
58
+ setSorting(sorting);
59
+ }}
60
+ onFilterChange={(_, filters) => {
61
+ setFilters(filters);
62
+ }}
63
+ registerRefresher={refresher => {
64
+ refreshRef.current = refresher;
65
+ registerRefresher?.(refresher);
66
+ }}
67
+ transformVariables={variables => {
68
+ const filter = variables.options?.filter ?? {};
69
+ return {
70
+ options: {
71
+ filter: {
72
+ ...filter,
73
+ groupId: { eq: productOptionGroupId },
74
+ },
75
+ sort: variables.options?.sort,
76
+ take: pageSize,
77
+ skip: (page - 1) * pageSize,
78
+ },
79
+ };
80
+ }}
81
+ onSearchTermChange={searchTerm => {
82
+ return {
83
+ name: {
84
+ contains: searchTerm,
85
+ },
86
+ };
87
+ }}
88
+ customizeColumns={{
89
+ name: {
90
+ header: 'Name',
91
+ cell: ({ row }) => (
92
+ <DetailPageButton
93
+ id={row.original.id}
94
+ label={row.original.name}
95
+ href={`options/${row.original.id}`}
96
+ />
97
+ ),
98
+ },
99
+ }}
100
+ />
101
+ <div className="mt-4">
102
+ <Button asChild variant="outline">
103
+ <Link to="./options/new">
104
+ <PlusIcon />
105
+ <Trans>Add product option</Trans>
106
+ </Link>
107
+ </Button>
108
+ </div>
109
+ </>
110
+ );
111
+ }
@@ -0,0 +1,103 @@
1
+ import { graphql } from '@/vdb/graphql/graphql.js';
2
+
3
+ export const productOptionGroupDetailDocument = graphql(`
4
+ query ProductOptionGroupDetail($id: ID!) {
5
+ productOptionGroup(id: $id) {
6
+ id
7
+ createdAt
8
+ updatedAt
9
+ name
10
+ code
11
+ languageCode
12
+ translations {
13
+ id
14
+ languageCode
15
+ name
16
+ }
17
+ customFields
18
+ }
19
+ }
20
+ `);
21
+
22
+ export const productIdNameDocument = graphql(`
23
+ query ProductIdName($id: ID!) {
24
+ product(id: $id) {
25
+ id
26
+ name
27
+ }
28
+ }
29
+ `);
30
+
31
+ export const productOptionGroupIdNameDocument = graphql(`
32
+ query ProductOptionGroupIdName($id: ID!) {
33
+ productOptionGroup(id: $id) {
34
+ id
35
+ name
36
+ }
37
+ }
38
+ `);
39
+
40
+ export const createProductOptionGroupDocument = graphql(`
41
+ mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
42
+ createProductOptionGroup(input: $input) {
43
+ id
44
+ }
45
+ }
46
+ `);
47
+
48
+ export const updateProductOptionGroupDocument = graphql(`
49
+ mutation UpdateProductOptionGroup($input: UpdateProductOptionGroupInput!) {
50
+ updateProductOptionGroup(input: $input) {
51
+ id
52
+ }
53
+ }
54
+ `);
55
+
56
+ export const productOptionDetailDocument = graphql(`
57
+ query ProductOptionDetail($id: ID!) {
58
+ productOption(id: $id) {
59
+ id
60
+ createdAt
61
+ updatedAt
62
+ name
63
+ code
64
+ languageCode
65
+ translations {
66
+ id
67
+ languageCode
68
+ name
69
+ }
70
+ group {
71
+ id
72
+ name
73
+ code
74
+ }
75
+ customFields
76
+ }
77
+ }
78
+ `);
79
+
80
+ export const createProductOptionDocument = graphql(`
81
+ mutation CreateProductOption($input: CreateProductOptionInput!) {
82
+ createProductOption(input: $input) {
83
+ id
84
+ }
85
+ }
86
+ `);
87
+
88
+ export const updateProductOptionDocument = graphql(`
89
+ mutation UpdateProductOption($input: UpdateProductOptionInput!) {
90
+ updateProductOption(input: $input) {
91
+ id
92
+ }
93
+ }
94
+ `);
95
+
96
+ export const deleteProductOptionDocument = graphql(`
97
+ mutation DeleteProductOption($id: ID!) {
98
+ deleteProductOption(id: $id) {
99
+ result
100
+ message
101
+ }
102
+ }
103
+ `);
@@ -44,7 +44,11 @@ export const productDetailFragment = graphql(
44
44
  slug
45
45
  description
46
46
  }
47
-
47
+ optionGroups {
48
+ id
49
+ code
50
+ name
51
+ }
48
52
  facetValues {
49
53
  id
50
54
  name
@@ -332,3 +336,11 @@ export const createProductVariantsDocument = graphql(`
332
336
  }
333
337
  }
334
338
  `);
339
+
340
+ export const reindexDocument = graphql(`
341
+ mutation Reindex {
342
+ reindex {
343
+ id
344
+ }
345
+ }
346
+ `);
@@ -3,9 +3,12 @@ import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
3
3
  import { Button } from '@/vdb/components/ui/button.js';
4
4
  import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
5
5
  import { ListPage } from '@/vdb/framework/page/list-page.js';
6
- import { Trans } from '@/vdb/lib/trans.js';
6
+ import { api } from '@/vdb/graphql/api.js';
7
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
8
+ import { useMutation } from '@tanstack/react-query';
7
9
  import { createFileRoute, Link } from '@tanstack/react-router';
8
- import { PlusIcon } from 'lucide-react';
10
+ import { PlusIcon, RefreshCwIcon } from 'lucide-react';
11
+ import { toast } from 'sonner';
9
12
  import {
10
13
  AssignFacetValuesToProductsBulkAction,
11
14
  AssignProductsToChannelBulkAction,
@@ -13,7 +16,7 @@ import {
13
16
  DuplicateProductsBulkAction,
14
17
  RemoveProductsFromChannelBulkAction,
15
18
  } from './components/product-bulk-actions.js';
16
- import { productListDocument } from './products.graphql.js';
19
+ import { productListDocument, reindexDocument } from './products.graphql.js';
17
20
 
18
21
  export const Route = createFileRoute('/_authenticated/_products/products')({
19
22
  component: ProductListPage,
@@ -21,6 +24,21 @@ export const Route = createFileRoute('/_authenticated/_products/products')({
21
24
  });
22
25
 
23
26
  function ProductListPage() {
27
+ const { i18n } = useLingui();
28
+ const reindexMutation = useMutation({
29
+ mutationFn: () => api.mutate(reindexDocument, {}),
30
+ onSuccess: () => {
31
+ toast.success(i18n.t('Search index rebuild started'));
32
+ },
33
+ onError: () => {
34
+ toast.error(i18n.t('Search index rebuild could not be started'));
35
+ },
36
+ });
37
+
38
+ const handleRebuildSearchIndex = () => {
39
+ reindexMutation.mutate();
40
+ };
41
+
24
42
  return (
25
43
  <ListPage
26
44
  pageId="product-list"
@@ -62,6 +80,12 @@ function ProductListPage() {
62
80
  ]}
63
81
  >
64
82
  <PageActionBarRight>
83
+ <PermissionGuard requires={['UpdateCatalog']}>
84
+ <Button variant="outline" onClick={handleRebuildSearchIndex}>
85
+ <RefreshCwIcon />
86
+ <Trans>Rebuild search index</Trans>
87
+ </Button>
88
+ </PermissionGuard>
65
89
  <PermissionGuard requires={['CreateProduct', 'CreateCatalog']}>
66
90
  <Button asChild>
67
91
  <Link to="./new">
@@ -1,4 +1,5 @@
1
1
  import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
2
+ import { SlugInput } from '@/vdb/components/data-input/slug-input.js';
2
3
  import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
3
4
  import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
4
5
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
@@ -6,7 +7,7 @@ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js'
6
7
  import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
7
8
  import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
8
9
  import { Button } from '@/vdb/components/ui/button.js';
9
- import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@/vdb/components/ui/form.js';
10
+ import { FormControl, FormDescription, FormItem, FormMessage } from '@/vdb/components/ui/form.js';
10
11
  import { Input } from '@/vdb/components/ui/input.js';
11
12
  import { Switch } from '@/vdb/components/ui/switch.js';
12
13
  import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
@@ -28,6 +29,7 @@ import { PlusIcon } from 'lucide-react';
28
29
  import { useRef } from 'react';
29
30
  import { toast } from 'sonner';
30
31
  import { CreateProductVariantsDialog } from './components/create-product-variants-dialog.js';
32
+ import { ProductOptionGroupBadge } from './components/product-option-group-badge.js';
31
33
  import { ProductVariantsTable } from './components/product-variants-table.js';
32
34
  import { createProductDocument, productDetailDocument, updateProductDocument } from './products.graphql.js';
33
35
 
@@ -81,7 +83,9 @@ function ProductDetailPage() {
81
83
  },
82
84
  params: { id: params.id },
83
85
  onSuccess: async data => {
84
- toast.success(i18n.t(creatingNewEntity ? 'Successfully created product' : 'Successfully updated product'));
86
+ toast.success(
87
+ i18n.t(creatingNewEntity ? 'Successfully created product' : 'Successfully updated product'),
88
+ );
85
89
  resetForm();
86
90
  if (creatingNewEntity) {
87
91
  await navigate({ to: `../$id`, params: { id: data.id } });
@@ -133,7 +137,15 @@ function ProductDetailPage() {
133
137
  control={form.control}
134
138
  name="slug"
135
139
  label={<Trans>Slug</Trans>}
136
- render={({ field }) => <Input {...field} />}
140
+ render={({ field }) => (
141
+ <SlugInput
142
+ {...field}
143
+ entityName="Product"
144
+ fieldName="slug"
145
+ watchFieldName="name"
146
+ entityId={entity?.id}
147
+ />
148
+ )}
137
149
  />
138
150
  </DetailFormGrid>
139
151
 
@@ -175,21 +187,26 @@ function ProductDetailPage() {
175
187
  />
176
188
  </PageBlock>
177
189
  )}
178
- <PageBlock column="side" blockId="facet-values">
190
+ {entity?.optionGroups.length ? (
191
+ <PageBlock column="side" blockId="option-groups" title={<Trans>Product Options</Trans>}>
192
+ <div className="flex flex-wrap gap-1.5">
193
+ {entity.optionGroups.map(g => (
194
+ <ProductOptionGroupBadge key={g.id} id={g.id} name={g.name} />
195
+ ))}
196
+ </div>
197
+ </PageBlock>
198
+ ) : null}
199
+ <PageBlock column="side" blockId="facet-values" title={<Trans>Facet Values</Trans>}>
179
200
  <FormFieldWrapper
180
201
  control={form.control}
181
202
  name="facetValueIds"
182
- label={<Trans>Facet values</Trans>}
183
203
  render={({ field }) => (
184
204
  <AssignedFacetValues facetValues={entity?.facetValues ?? []} {...field} />
185
205
  )}
186
206
  />
187
207
  </PageBlock>
188
- <PageBlock column="side" blockId="assets">
208
+ <PageBlock column="side" blockId="assets" title={<Trans>Assets</Trans>}>
189
209
  <FormItem>
190
- <FormLabel>
191
- <Trans>Assets</Trans>
192
- </FormLabel>
193
210
  <FormControl>
194
211
  <EntityAssets
195
212
  assets={entity?.assets}
@@ -0,0 +1,181 @@
1
+ import { SlugInput } from '@/vdb/components/data-input/index.js';
2
+ import { ErrorPage } from '@/vdb/components/shared/error-page.js';
3
+ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
4
+ import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
5
+ import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
6
+ import { Button } from '@/vdb/components/ui/button.js';
7
+ import { Input } from '@/vdb/components/ui/input.js';
8
+ import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
9
+ import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
10
+ import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
11
+ import {
12
+ CustomFieldsPageBlock,
13
+ DetailFormGrid,
14
+ Page,
15
+ PageActionBar,
16
+ PageActionBarRight,
17
+ PageBlock,
18
+ PageLayout,
19
+ PageTitle,
20
+ } from '@/vdb/framework/layout-engine/page-layout.js';
21
+ import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
22
+ import { api } from '@/vdb/graphql/api.js';
23
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
24
+ import { createFileRoute, ParsedLocation, useNavigate } from '@tanstack/react-router';
25
+ import { toast } from 'sonner';
26
+ import { ProductOptionsTable } from './components/product-options-table.js';
27
+ import {
28
+ createProductOptionGroupDocument,
29
+ productIdNameDocument,
30
+ productOptionGroupDetailDocument,
31
+ updateProductOptionGroupDocument,
32
+ } from './product-option-groups.graphql.js';
33
+
34
+ const pageId = 'product-option-group-detail';
35
+
36
+ export const Route = createFileRoute('/_authenticated/_products/products_/$productId/option-groups/$id')({
37
+ component: ProductOptionGroupDetailPage,
38
+ loader: async ({ context, params }: { context: any; params: any; location: ParsedLocation }) => {
39
+ if (!params.id) {
40
+ throw new Error('ID param is required');
41
+ }
42
+
43
+ const { extendedQuery: extendedQueryDocument } = extendDetailFormQuery(
44
+ addCustomFields(productOptionGroupDetailDocument),
45
+ pageId,
46
+ );
47
+ const result = await context.queryClient.ensureQueryData(
48
+ getDetailQueryOptions(extendedQueryDocument, { id: params.id }),
49
+ );
50
+ const productResult = await context.queryClient.fetchQuery({
51
+ queryKey: [pageId, 'productIdName', params.productId],
52
+ queryFn: () => api.query(productIdNameDocument, { id: params.productId }),
53
+ });
54
+ const entityName = 'ProductOptionGroup';
55
+
56
+ if (!result.productOptionGroup) {
57
+ throw new Error(`${entityName} with the ID ${params.id} was not found`);
58
+ }
59
+ return {
60
+ breadcrumb: [
61
+ { path: '/products', label: <Trans>Products</Trans> },
62
+ { path: `/products/${productResult.product.id}`, label: productResult.product.name },
63
+ { path: `/products/${productResult.product.id}`, label: <Trans>Option Groups</Trans> },
64
+ result.productOptionGroup?.name,
65
+ ],
66
+ };
67
+ },
68
+ errorComponent: ({ error }) => <ErrorPage message={error.message} />,
69
+ });
70
+
71
+ function ProductOptionGroupDetailPage() {
72
+ const params = Route.useParams();
73
+ const navigate = useNavigate();
74
+ const creatingNewEntity = params.id === NEW_ENTITY_PATH;
75
+ const { i18n } = useLingui();
76
+
77
+ const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
78
+ pageId,
79
+ queryDocument: productOptionGroupDetailDocument,
80
+ createDocument: createProductOptionGroupDocument,
81
+ updateDocument: updateProductOptionGroupDocument,
82
+ setValuesForUpdate: entity => {
83
+ return {
84
+ id: entity.id,
85
+ code: entity.code,
86
+ translations: entity.translations.map(translation => ({
87
+ id: translation.id,
88
+ languageCode: translation.languageCode,
89
+ name: translation.name,
90
+ customFields: (translation as any).customFields,
91
+ })),
92
+ options: [],
93
+ customFields: entity.customFields,
94
+ };
95
+ },
96
+ transformCreateInput: values => {
97
+ return {
98
+ ...values,
99
+ options: [],
100
+ };
101
+ },
102
+ params: { id: params.id },
103
+ onSuccess: async data => {
104
+ toast(
105
+ i18n.t(
106
+ creatingNewEntity
107
+ ? 'Successfully created product option group'
108
+ : 'Successfully updated product option group',
109
+ ),
110
+ );
111
+ resetForm();
112
+ if (creatingNewEntity) {
113
+ await navigate({ to: `../$id`, params: { id: data.id } });
114
+ }
115
+ },
116
+ onError: err => {
117
+ toast(
118
+ i18n.t(
119
+ creatingNewEntity
120
+ ? 'Failed to create product option group'
121
+ : 'Failed to update product option group',
122
+ ),
123
+ {
124
+ description: err instanceof Error ? err.message : 'Unknown error',
125
+ },
126
+ );
127
+ },
128
+ });
129
+
130
+ return (
131
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
132
+ <PageTitle>
133
+ {creatingNewEntity ? <Trans>New product option group</Trans> : (entity?.name ?? '')}
134
+ </PageTitle>
135
+ <PageActionBar>
136
+ <PageActionBarRight>
137
+ <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
138
+ <Button
139
+ type="submit"
140
+ disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
141
+ >
142
+ {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
143
+ </Button>
144
+ </PermissionGuard>
145
+ </PageActionBarRight>
146
+ </PageActionBar>
147
+ <PageLayout>
148
+ <PageBlock column="main" blockId="main-form">
149
+ <DetailFormGrid>
150
+ <TranslatableFormFieldWrapper
151
+ control={form.control}
152
+ name="name"
153
+ label={<Trans>Name</Trans>}
154
+ render={({ field }) => <Input {...field} />}
155
+ />
156
+ <FormFieldWrapper
157
+ control={form.control}
158
+ name="code"
159
+ label={<Trans>Code</Trans>}
160
+ render={({ field }) => (
161
+ <SlugInput
162
+ fieldName="code"
163
+ watchFieldName="name"
164
+ entityName="ProductOptionGroup"
165
+ entityId={entity?.id}
166
+ {...field}
167
+ />
168
+ )}
169
+ />
170
+ </DetailFormGrid>
171
+ </PageBlock>
172
+ <CustomFieldsPageBlock column="main" entityType="ProductOptionGroup" control={form.control} />
173
+ {entity && (
174
+ <PageBlock column="main" blockId="product-options" title={<Trans>Product Options</Trans>}>
175
+ <ProductOptionsTable productOptionGroupId={entity?.id} />
176
+ </PageBlock>
177
+ )}
178
+ </PageLayout>
179
+ </Page>
180
+ );
181
+ }