@vendure/dashboard 3.4.3-master-202509250229 → 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.
- package/dist/plugin/api/api-extensions.js +11 -14
- package/dist/plugin/api/metrics.resolver.d.ts +2 -2
- package/dist/plugin/api/metrics.resolver.js +2 -2
- package/dist/plugin/config/metrics-strategies.d.ts +9 -9
- package/dist/plugin/config/metrics-strategies.js +6 -6
- package/dist/plugin/constants.d.ts +2 -0
- package/dist/plugin/constants.js +3 -1
- package/dist/plugin/dashboard.plugin.js +13 -0
- package/dist/plugin/service/metrics.service.d.ts +3 -3
- package/dist/plugin/service/metrics.service.js +37 -53
- package/dist/plugin/types.d.ts +9 -12
- package/dist/plugin/types.js +7 -11
- package/dist/vite/utils/compiler.js +2 -0
- package/dist/vite/vite-plugin-vendure-dashboard.js +2 -2
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_collections/collections.tsx +7 -2
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +15 -2
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +14 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +10 -0
- package/src/app/routes/_authenticated/_products/components/product-option-group-badge.tsx +19 -0
- package/src/app/routes/_authenticated/_products/components/product-options-table.tsx +111 -0
- package/src/app/routes/_authenticated/_products/product-option-groups.graphql.ts +103 -0
- package/src/app/routes/_authenticated/_products/products.graphql.ts +13 -1
- package/src/app/routes/_authenticated/_products/products.tsx +27 -3
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +26 -9
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +181 -0
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +208 -0
- package/src/app/routes/_authenticated/_zones/components/zone-countries-sheet.tsx +4 -1
- package/src/app/routes/_authenticated/index.tsx +41 -24
- package/src/lib/components/data-display/json.tsx +16 -1
- package/src/lib/components/data-input/index.ts +3 -0
- package/src/lib/components/data-input/slug-input.tsx +296 -0
- package/src/lib/components/data-table/add-filter-menu.tsx +13 -6
- package/src/lib/components/data-table/data-table-bulk-action-item.tsx +38 -1
- package/src/lib/components/data-table/data-table-context.tsx +91 -0
- package/src/lib/components/data-table/data-table-filter-badge.tsx +9 -5
- package/src/lib/components/data-table/data-table-view-options.tsx +17 -8
- package/src/lib/components/data-table/data-table.tsx +146 -94
- package/src/lib/components/data-table/global-views-bar.tsx +97 -0
- package/src/lib/components/data-table/global-views-sheet.tsx +11 -0
- package/src/lib/components/data-table/manage-global-views-button.tsx +26 -0
- package/src/lib/components/data-table/my-views-button.tsx +47 -0
- package/src/lib/components/data-table/refresh-button.tsx +12 -3
- package/src/lib/components/data-table/save-view-button.tsx +45 -0
- package/src/lib/components/data-table/save-view-dialog.tsx +113 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +3 -1
- package/src/lib/components/data-table/user-views-sheet.tsx +11 -0
- package/src/lib/components/data-table/views-sheet.tsx +297 -0
- package/src/lib/components/date-range-picker.tsx +184 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +59 -32
- package/src/lib/components/ui/button.tsx +1 -1
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +29 -2
- package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +10 -7
- package/src/lib/framework/dashboard-widget/metrics-widget/metrics-widget.graphql.ts +9 -3
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +19 -75
- package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +33 -0
- package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +319 -9
- package/src/lib/framework/document-introspection/add-custom-fields.ts +60 -31
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +1 -159
- package/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts +1840 -0
- package/src/lib/framework/document-introspection/include-only-selected-list-fields.ts +940 -0
- package/src/lib/framework/document-introspection/testing-utils.ts +161 -0
- package/src/lib/framework/extension-api/display-component-extensions.tsx +2 -0
- package/src/lib/framework/extension-api/types/data-table.ts +62 -4
- package/src/lib/framework/extension-api/types/navigation.ts +16 -0
- package/src/lib/framework/form-engine/utils.ts +34 -0
- package/src/lib/framework/page/list-page.tsx +289 -4
- package/src/lib/framework/page/use-extended-router.tsx +59 -17
- package/src/lib/graphql/api.ts +4 -2
- package/src/lib/graphql/graphql-env.d.ts +13 -10
- package/src/lib/hooks/use-extended-list-query.ts +5 -0
- package/src/lib/hooks/use-saved-views.ts +230 -0
- package/src/lib/index.ts +15 -0
- package/src/lib/types/saved-views.ts +39 -0
- 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 {
|
|
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,
|
|
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(
|
|
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 }) =>
|
|
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
|
-
|
|
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
|
+
}
|