@vendure/dashboard 3.3.6-master-202507010243 → 3.3.6-master-202507010922

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 (49) hide show
  1. package/package.json +4 -4
  2. package/src/app/common/duplicate-bulk-action.tsx +134 -0
  3. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +5 -1
  4. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +7 -2
  5. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +5 -1
  6. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +9 -0
  7. package/src/app/routes/_authenticated/_collections/collections.tsx +10 -0
  8. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +5 -1
  9. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +66 -6
  10. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +1 -1
  11. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -5
  12. package/src/app/routes/_authenticated/_customer-groups/customer-groups.graphql.ts +1 -1
  13. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +8 -5
  14. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +5 -1
  15. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +5 -1
  16. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +5 -2
  17. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +5 -1
  18. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -5
  19. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +5 -1
  20. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +19 -106
  21. package/src/app/routes/_authenticated/_products/products.graphql.ts +0 -17
  22. package/src/app/routes/_authenticated/_products/products_.$id.tsx +5 -1
  23. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +5 -1
  24. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -1
  25. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +6 -2
  26. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +5 -1
  27. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +5 -1
  28. package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +1 -1
  29. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -5
  30. package/src/app/routes/_authenticated/_tax-rates/tax-rates.graphql.ts +1 -1
  31. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +8 -4
  32. package/src/app/routes/_authenticated/_zones/zones.graphql.ts +1 -1
  33. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +8 -4
  34. package/src/lib/components/shared/custom-fields-form.tsx +18 -1
  35. package/src/lib/framework/document-extension/extend-detail-form-query.ts +50 -0
  36. package/src/lib/framework/document-extension/extend-document.spec.ts +335 -0
  37. package/src/lib/framework/document-introspection/add-custom-fields.ts +48 -0
  38. package/src/lib/framework/extension-api/define-dashboard-extension.ts +19 -1
  39. package/src/lib/framework/extension-api/extension-api-types.ts +15 -2
  40. package/src/lib/framework/form-engine/custom-form-component-extensions.ts +13 -3
  41. package/src/lib/framework/form-engine/utils.ts +43 -15
  42. package/src/lib/framework/layout-engine/page-layout.tsx +1 -0
  43. package/src/lib/framework/page/detail-page-route-loader.tsx +13 -1
  44. package/src/lib/framework/page/use-detail-page.ts +11 -2
  45. package/src/lib/framework/registry/registry-types.ts +1 -0
  46. package/src/lib/graphql/common-operations.ts +18 -0
  47. package/src/lib/graphql/{fragments.tsx → fragments.ts} +1 -2
  48. package/src/lib/graphql/graphql-env.d.ts +10 -8
  49. package/src/lib/hooks/use-extended-detail-query.ts +37 -0
@@ -10,7 +10,6 @@ import { ResultOf } from '@/graphql/graphql.js';
10
10
  import { useChannel, usePaginatedList } from '@/index.js';
11
11
  import { Trans, useLingui } from '@/lib/trans.js';
12
12
 
13
- import { Permission } from '@vendure/common/lib/generated-types';
14
13
  import { AssignFacetValuesDialog } from '../../_products/components/assign-facet-values-dialog.js';
15
14
  import { AssignToChannelDialog } from '../../_products/components/assign-to-channel-dialog.js';
16
15
  import {
@@ -52,7 +51,7 @@ export const DeleteProductVariantsBulkAction: BulkActionComponent<any> = ({ sele
52
51
  });
53
52
  return (
54
53
  <DataTableBulkActionItem
55
- requiresPermission={[Permission.DeleteCatalog, Permission.DeleteProduct]}
54
+ requiresPermission={['DeleteCatalog', 'DeleteProduct']}
56
55
  onClick={() => mutate({ ids: selection.map(s => s.id) })}
57
56
  label={<Trans>Delete</Trans>}
58
57
  confirmationText={
@@ -81,7 +80,7 @@ export const AssignProductVariantsToChannelBulkAction: BulkActionComponent<any>
81
80
  return (
82
81
  <>
83
82
  <DataTableBulkActionItem
84
- requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
83
+ requiresPermission={['UpdateCatalog', 'UpdateProduct']}
85
84
  onClick={() => setDialogOpen(true)}
86
85
  label={<Trans>Assign to channel</Trans>}
87
86
  icon={LayersIcon}
@@ -134,7 +133,7 @@ export const RemoveProductVariantsFromChannelBulkAction: BulkActionComponent<any
134
133
 
135
134
  return (
136
135
  <DataTableBulkActionItem
137
- requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
136
+ requiresPermission={['UpdateCatalog', 'UpdateProduct']}
138
137
  onClick={handleRemove}
139
138
  label={<Trans>Remove from current channel</Trans>}
140
139
  confirmationText={
@@ -164,7 +163,7 @@ export const AssignFacetValuesToProductVariantsBulkAction: BulkActionComponent<a
164
163
  return (
165
164
  <>
166
165
  <DataTableBulkActionItem
167
- requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
166
+ requiresPermission={['UpdateCatalog', 'UpdateProduct']}
168
167
  onClick={() => setDialogOpen(true)}
169
168
  label={<Trans>Edit facet values</Trans>}
170
169
  icon={TagIcon}
@@ -36,9 +36,12 @@ import {
36
36
  updateProductVariantDocument,
37
37
  } from './product-variants.graphql.js';
38
38
 
39
+ const pageId = 'product-variant-detail';
40
+
39
41
  export const Route = createFileRoute('/_authenticated/_product-variants/product-variants_/$id')({
40
42
  component: ProductVariantDetailPage,
41
43
  loader: detailPageRouteLoader({
44
+ pageId,
42
45
  queryDocument: productVariantDetailDocument,
43
46
  breadcrumb(_isNew, entity, location) {
44
47
  if ((location.search as any).from === 'product') {
@@ -62,6 +65,7 @@ function ProductVariantDetailPage() {
62
65
  const { activeChannel } = useChannel();
63
66
 
64
67
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
68
+ pageId,
65
69
  queryDocument: productVariantDetailDocument,
66
70
  createDocument: createProductVariantDocument,
67
71
  updateDocument: updateProductVariantDocument,
@@ -109,7 +113,7 @@ function ProductVariantDetailPage() {
109
113
  const [price, taxCategoryId] = form.watch(['price', 'taxCategoryId']);
110
114
 
111
115
  return (
112
- <Page pageId="product-variant-detail" form={form} submitHandler={submitHandler} entity={entity}>
116
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
113
117
  <PageTitle>
114
118
  {creatingNewEntity ? <Trans>New product variant</Trans> : (entity?.name ?? '')}
115
119
  </PageTitle>
@@ -1,5 +1,5 @@
1
1
  import { useMutation } from '@tanstack/react-query';
2
- import { CopyIcon, LayersIcon, TagIcon, TrashIcon } from 'lucide-react';
2
+ import { LayersIcon, TagIcon, TrashIcon } from 'lucide-react';
3
3
  import { useState } from 'react';
4
4
  import { toast } from 'sonner';
5
5
 
@@ -9,12 +9,10 @@ import { api } from '@/graphql/api.js';
9
9
  import { ResultOf } from '@/graphql/graphql.js';
10
10
  import { useChannel, usePaginatedList } from '@/index.js';
11
11
  import { Trans, useLingui } from '@/lib/trans.js';
12
-
13
- import { Permission } from '@vendure/common/lib/generated-types';
12
+ import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
14
13
  import {
15
14
  assignProductsToChannelDocument,
16
15
  deleteProductsDocument,
17
- duplicateEntityDocument,
18
16
  getProductsWithFacetValuesByIdsDocument,
19
17
  productDetailDocument,
20
18
  removeProductsFromChannelDocument,
@@ -53,7 +51,7 @@ export const DeleteProductsBulkAction: BulkActionComponent<any> = ({ selection,
53
51
  });
54
52
  return (
55
53
  <DataTableBulkActionItem
56
- requiresPermission={[Permission.DeleteCatalog, Permission.DeleteProduct]}
54
+ requiresPermission={['DeleteCatalog', 'DeleteProduct']}
57
55
  onClick={() => mutate({ ids: selection.map(s => s.id) })}
58
56
  label={<Trans>Delete</Trans>}
59
57
  confirmationText={<Trans>Are you sure you want to delete {selection.length} products?</Trans>}
@@ -80,7 +78,7 @@ export const AssignProductsToChannelBulkAction: BulkActionComponent<any> = ({ se
80
78
  return (
81
79
  <>
82
80
  <DataTableBulkActionItem
83
- requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
81
+ requiresPermission={['UpdateCatalog', 'UpdateProduct']}
84
82
  onClick={() => setDialogOpen(true)}
85
83
  label={<Trans>Assign to channel</Trans>}
86
84
  icon={LayersIcon}
@@ -128,7 +126,7 @@ export const RemoveProductsFromChannelBulkAction: BulkActionComponent<any> = ({
128
126
 
129
127
  return (
130
128
  <DataTableBulkActionItem
131
- requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
129
+ requiresPermission={['UpdateCatalog', 'UpdateProduct']}
132
130
  onClick={handleRemove}
133
131
  label={<Trans>Remove from current channel</Trans>}
134
132
  confirmationText={
@@ -154,7 +152,7 @@ export const AssignFacetValuesToProductsBulkAction: BulkActionComponent<any> = (
154
152
  return (
155
153
  <>
156
154
  <DataTableBulkActionItem
157
- requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
155
+ requiresPermission={['UpdateCatalog', 'UpdateProduct']}
158
156
  onClick={() => setDialogOpen(true)}
159
157
  label={<Trans>Edit facet values</Trans>}
160
158
  icon={TagIcon}
@@ -174,105 +172,20 @@ export const AssignFacetValuesToProductsBulkAction: BulkActionComponent<any> = (
174
172
  };
175
173
 
176
174
  export const DuplicateProductsBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
177
- const { refetchPaginatedList } = usePaginatedList();
178
- const { i18n } = useLingui();
179
- const [isDuplicating, setIsDuplicating] = useState(false);
180
- const [progress, setProgress] = useState({ completed: 0, total: 0 });
181
-
182
- const { mutateAsync } = useMutation({
183
- mutationFn: api.mutate(duplicateEntityDocument),
184
- });
185
-
186
- const handleDuplicate = async () => {
187
- if (isDuplicating) return;
188
-
189
- setIsDuplicating(true);
190
- setProgress({ completed: 0, total: selection.length });
191
-
192
- const results = {
193
- success: 0,
194
- failed: 0,
195
- errors: [] as string[],
196
- };
197
-
198
- try {
199
- // Process products sequentially to avoid overwhelming the server
200
- for (let i = 0; i < selection.length; i++) {
201
- const product = selection[i];
202
-
203
- try {
204
- const result = await mutateAsync({
205
- input: {
206
- entityName: 'Product',
207
- entityId: product.id,
208
- duplicatorInput: {
209
- code: 'product-duplicator',
210
- arguments: [
211
- {
212
- name: 'includeVariants',
213
- value: 'true',
214
- },
215
- ],
216
- },
217
- },
218
- });
219
-
220
- if ('newEntityId' in result.duplicateEntity) {
221
- results.success++;
222
- } else {
223
- results.failed++;
224
- const errorMsg =
225
- result.duplicateEntity.message ||
226
- result.duplicateEntity.duplicationError ||
227
- 'Unknown error';
228
- results.errors.push(`Product ${product.name || product.id}: ${errorMsg}`);
229
- }
230
- } catch (error) {
231
- results.failed++;
232
- results.errors.push(
233
- `Product ${product.name || product.id}: ${error instanceof Error ? error.message : 'Unknown error'}`,
234
- );
235
- }
236
-
237
- setProgress({ completed: i + 1, total: selection.length });
238
- }
239
-
240
- // Show results
241
- if (results.success > 0) {
242
- toast.success(i18n.t(`Successfully duplicated ${results.success} products`));
243
- }
244
- if (results.failed > 0) {
245
- const errorMessage =
246
- results.errors.length > 3
247
- ? `${results.errors.slice(0, 3).join(', ')}... and ${results.errors.length - 3} more`
248
- : results.errors.join(', ');
249
- toast.error(`Failed to duplicate ${results.failed} products: ${errorMessage}`);
250
- }
251
-
252
- if (results.success > 0) {
253
- refetchPaginatedList();
254
- table.resetRowSelection();
255
- }
256
- } finally {
257
- setIsDuplicating(false);
258
- setProgress({ completed: 0, total: 0 });
259
- }
260
- };
261
-
262
175
  return (
263
- <DataTableBulkActionItem
264
- requiresPermission={[Permission.UpdateCatalog, Permission.UpdateProduct]}
265
- onClick={handleDuplicate}
266
- label={
267
- isDuplicating ? (
268
- <Trans>
269
- Duplicating... ({progress.completed}/{progress.total})
270
- </Trans>
271
- ) : (
272
- <Trans>Duplicate</Trans>
273
- )
274
- }
275
- icon={CopyIcon}
176
+ <DuplicateBulkAction
177
+ entityType="Product"
178
+ duplicatorCode="product-duplicator"
179
+ duplicatorArguments={[
180
+ {
181
+ name: 'includeVariants',
182
+ value: 'true',
183
+ },
184
+ ]}
185
+ requiredPermissions={['UpdateCatalog', 'UpdateProduct']}
186
+ entityName="Product"
187
+ selection={selection}
188
+ table={table}
276
189
  />
277
190
  );
278
191
  };
@@ -187,20 +187,3 @@ export const getProductsWithFacetValuesByIdsDocument = graphql(`
187
187
  }
188
188
  }
189
189
  `);
190
-
191
- export const duplicateEntityDocument = graphql(`
192
- mutation DuplicateEntity($input: DuplicateEntityInput!) {
193
- duplicateEntity(input: $input) {
194
- ... on DuplicateEntitySuccess {
195
- newEntityId
196
- }
197
- ... on ErrorResult {
198
- errorCode
199
- message
200
- }
201
- ... on DuplicateEntityError {
202
- duplicationError
203
- }
204
- }
205
- }
206
- `);
@@ -31,9 +31,12 @@ import { CreateProductVariantsDialog } from './components/create-product-variant
31
31
  import { ProductVariantsTable } from './components/product-variants-table.js';
32
32
  import { createProductDocument, productDetailDocument, updateProductDocument } from './products.graphql.js';
33
33
 
34
+ const pageId = 'product-detail';
35
+
34
36
  export const Route = createFileRoute('/_authenticated/_products/products_/$id')({
35
37
  component: ProductDetailPage,
36
38
  loader: detailPageRouteLoader({
39
+ pageId,
37
40
  queryDocument: productDetailDocument,
38
41
  breadcrumb(isNew, entity) {
39
42
  return [
@@ -53,6 +56,7 @@ function ProductDetailPage() {
53
56
  const refreshRef = useRef<() => void>(() => {});
54
57
 
55
58
  const { form, submitHandler, entity, isPending, refreshEntity, resetForm } = useDetailPage({
59
+ pageId,
56
60
  entityName: 'Product',
57
61
  queryDocument: productDetailDocument,
58
62
  createDocument: createProductDocument,
@@ -91,7 +95,7 @@ function ProductDetailPage() {
91
95
  });
92
96
 
93
97
  return (
94
- <Page pageId="product-detail" form={form} submitHandler={submitHandler} entity={entity}>
98
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
95
99
  <PageTitle>{creatingNewEntity ? <Trans>New product</Trans> : (entity?.name ?? '')}</PageTitle>
96
100
  <PageActionBar>
97
101
  <PageActionBarRight>
@@ -31,9 +31,12 @@ import {
31
31
  updatePromotionDocument,
32
32
  } from './promotions.graphql.js';
33
33
 
34
+ const pageId = 'promotion-detail';
35
+
34
36
  export const Route = createFileRoute('/_authenticated/_promotions/promotions_/$id')({
35
37
  component: PromotionDetailPage,
36
38
  loader: detailPageRouteLoader({
39
+ pageId,
37
40
  queryDocument: promotionDetailDocument,
38
41
  breadcrumb(isNew, entity) {
39
42
  return [
@@ -52,6 +55,7 @@ function PromotionDetailPage() {
52
55
  const { i18n } = useLingui();
53
56
 
54
57
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
58
+ pageId,
55
59
  queryDocument: promotionDetailDocument,
56
60
  createDocument: createPromotionDocument,
57
61
  transformCreateInput: values => {
@@ -114,7 +118,7 @@ function PromotionDetailPage() {
114
118
  });
115
119
 
116
120
  return (
117
- <Page pageId="promotion-detail" form={form} submitHandler={submitHandler} entity={entity}>
121
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
118
122
  <PageTitle>{creatingNewEntity ? <Trans>New promotion</Trans> : (entity?.name ?? '')}</PageTitle>
119
123
  <PageActionBar>
120
124
  <PageActionBarRight>
@@ -22,9 +22,12 @@ import { toast } from 'sonner';
22
22
  import { PermissionsGrid } from './components/permissions-grid.js';
23
23
  import { createRoleDocument, roleDetailDocument, updateRoleDocument } from './roles.graphql.js';
24
24
 
25
+ const pageId = 'role-detail';
26
+
25
27
  export const Route = createFileRoute('/_authenticated/_roles/roles_/$id')({
26
28
  component: RoleDetailPage,
27
29
  loader: detailPageRouteLoader({
30
+ pageId,
28
31
  queryDocument: roleDetailDocument,
29
32
  breadcrumb(isNew, entity) {
30
33
  return [
@@ -43,6 +46,7 @@ function RoleDetailPage() {
43
46
  const { i18n } = useLingui();
44
47
 
45
48
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
49
+ pageId,
46
50
  queryDocument: roleDetailDocument,
47
51
  createDocument: createRoleDocument,
48
52
  updateDocument: updateRoleDocument,
@@ -71,7 +75,7 @@ function RoleDetailPage() {
71
75
  });
72
76
 
73
77
  return (
74
- <Page pageId="role-detail" form={form} submitHandler={submitHandler} entity={entity}>
78
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
75
79
  <PageTitle>{creatingNewEntity ? <Trans>New role</Trans> : (entity?.description ?? '')}</PageTitle>
76
80
  <PageActionBar>
77
81
  <PageActionBarRight>
@@ -20,9 +20,12 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
20
20
  import { toast } from 'sonner';
21
21
  import { createSellerDocument, sellerDetailDocument, updateSellerDocument } from './sellers.graphql.js';
22
22
 
23
+ const pageId = 'seller-detail';
24
+
23
25
  export const Route = createFileRoute('/_authenticated/_sellers/sellers_/$id')({
24
26
  component: SellerDetailPage,
25
27
  loader: detailPageRouteLoader({
28
+ pageId,
26
29
  queryDocument: sellerDetailDocument,
27
30
  breadcrumb: (isNew, entity) => [
28
31
  { path: '/sellers', label: 'Sellers' },
@@ -38,7 +41,8 @@ function SellerDetailPage() {
38
41
  const creatingNewEntity = params.id === NEW_ENTITY_PATH;
39
42
  const { i18n } = useLingui();
40
43
 
41
- const { form, submitHandler, entity, isPending } = useDetailPage({
44
+ const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
45
+ pageId,
42
46
  queryDocument: sellerDetailDocument,
43
47
  createDocument: createSellerDocument,
44
48
  updateDocument: updateSellerDocument,
@@ -65,7 +69,7 @@ function SellerDetailPage() {
65
69
  });
66
70
 
67
71
  return (
68
- <Page pageId="seller-detail" form={form} submitHandler={submitHandler} entity={entity}>
72
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
69
73
  <PageTitle>{creatingNewEntity ? <Trans>New seller</Trans> : (entity?.name ?? '')}</PageTitle>
70
74
  <PageActionBar>
71
75
  <PageActionBarRight>
@@ -30,9 +30,12 @@ import {
30
30
  updateShippingMethodDocument,
31
31
  } from './shipping-methods.graphql.js';
32
32
 
33
+ const pageId = 'shipping-method-detail';
34
+
33
35
  export const Route = createFileRoute('/_authenticated/_shipping-methods/shipping-methods_/$id')({
34
36
  component: ShippingMethodDetailPage,
35
37
  loader: detailPageRouteLoader({
38
+ pageId,
36
39
  queryDocument: shippingMethodDetailDocument,
37
40
  breadcrumb(isNew, entity) {
38
41
  return [
@@ -51,6 +54,7 @@ function ShippingMethodDetailPage() {
51
54
  const { i18n } = useLingui();
52
55
 
53
56
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
57
+ pageId,
54
58
  queryDocument: shippingMethodDetailDocument,
55
59
  createDocument: createShippingMethodDocument,
56
60
  updateDocument: updateShippingMethodDocument,
@@ -94,7 +98,7 @@ function ShippingMethodDetailPage() {
94
98
  });
95
99
 
96
100
  return (
97
- <Page pageId="shipping-method-detail" form={form} submitHandler={submitHandler} entity={entity}>
101
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
98
102
  <PageTitle>
99
103
  {creatingNewEntity ? <Trans>New shipping method</Trans> : (entity?.name ?? '')}
100
104
  </PageTitle>
@@ -26,9 +26,12 @@ import {
26
26
  updateStockLocationDocument,
27
27
  } from './stock-locations.graphql.js';
28
28
 
29
+ const pageId = 'stock-location-detail';
30
+
29
31
  export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations_/$id')({
30
32
  component: StockLocationDetailPage,
31
33
  loader: detailPageRouteLoader({
34
+ pageId,
32
35
  queryDocument: stockLocationDetailQuery,
33
36
  breadcrumb(isNew, entity) {
34
37
  return [
@@ -47,6 +50,7 @@ function StockLocationDetailPage() {
47
50
  const { i18n } = useLingui();
48
51
 
49
52
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
53
+ pageId,
50
54
  queryDocument: stockLocationDetailQuery,
51
55
  createDocument: createStockLocationDocument,
52
56
  updateDocument: updateStockLocationDocument,
@@ -74,7 +78,7 @@ function StockLocationDetailPage() {
74
78
  });
75
79
 
76
80
  return (
77
- <Page pageId="stock-location-detail" form={form} submitHandler={submitHandler} entity={entity}>
81
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
78
82
  <PageTitle>
79
83
  {creatingNewEntity ? <Trans>New stock location</Trans> : (entity?.name ?? '')}
80
84
  </PageTitle>
@@ -24,7 +24,7 @@ export const taxCategoryListQuery = graphql(
24
24
  [taxCategoryItemFragment],
25
25
  );
26
26
 
27
- export const taxCategoryDetailQuery = graphql(`
27
+ export const taxCategoryDetailDocument = graphql(`
28
28
  query TaxCategoryDetail($id: ID!) {
29
29
  taxCategory(id: $id) {
30
30
  id
@@ -22,14 +22,17 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
22
22
  import { toast } from 'sonner';
23
23
  import {
24
24
  createTaxCategoryDocument,
25
- taxCategoryDetailQuery,
25
+ taxCategoryDetailDocument,
26
26
  updateTaxCategoryDocument,
27
27
  } from './tax-categories.graphql.js';
28
28
 
29
+ const pageId = 'tax-category-detail';
30
+
29
31
  export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories_/$id')({
30
32
  component: TaxCategoryDetailPage,
31
33
  loader: detailPageRouteLoader({
32
- queryDocument: taxCategoryDetailQuery,
34
+ pageId,
35
+ queryDocument: taxCategoryDetailDocument,
33
36
  breadcrumb(isNew, entity) {
34
37
  return [
35
38
  { path: '/tax-categories', label: 'Tax categories' },
@@ -46,8 +49,9 @@ function TaxCategoryDetailPage() {
46
49
  const creatingNewEntity = params.id === NEW_ENTITY_PATH;
47
50
  const { i18n } = useLingui();
48
51
 
49
- const { form, submitHandler, entity, isPending } = useDetailPage({
50
- queryDocument: taxCategoryDetailQuery,
52
+ const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
53
+ pageId,
54
+ queryDocument: taxCategoryDetailDocument,
51
55
  createDocument: createTaxCategoryDocument,
52
56
  updateDocument: updateTaxCategoryDocument,
53
57
  setValuesForUpdate: entity => {
@@ -73,7 +77,7 @@ function TaxCategoryDetailPage() {
73
77
  });
74
78
 
75
79
  return (
76
- <Page pageId="tax-category-detail" form={form} submitHandler={submitHandler} entity={entity}>
80
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
77
81
  <PageTitle>
78
82
  {creatingNewEntity ? <Trans>New tax category</Trans> : (entity?.name ?? '')}
79
83
  </PageTitle>
@@ -37,7 +37,7 @@ export const taxRateListQuery = graphql(
37
37
  [taxRateItemFragment],
38
38
  );
39
39
 
40
- export const taxRateDetailQuery = graphql(
40
+ export const taxRateDetailDocument = graphql(
41
41
  `
42
42
  query TaxRateDetail($id: ID!) {
43
43
  taxRate(id: $id) {
@@ -23,12 +23,15 @@ import { useDetailPage } from '@/framework/page/use-detail-page.js';
23
23
  import { Trans, useLingui } from '@/lib/trans.js';
24
24
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
25
25
  import { toast } from 'sonner';
26
- import { createTaxRateDocument, taxRateDetailQuery, updateTaxRateDocument } from './tax-rates.graphql.js';
26
+ import { createTaxRateDocument, taxRateDetailDocument, updateTaxRateDocument } from './tax-rates.graphql.js';
27
+
28
+ const pageId = 'tax-rate-detail';
27
29
 
28
30
  export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates_/$id')({
29
31
  component: TaxRateDetailPage,
30
32
  loader: detailPageRouteLoader({
31
- queryDocument: taxRateDetailQuery,
33
+ pageId,
34
+ queryDocument: taxRateDetailDocument,
32
35
  breadcrumb(isNew, entity) {
33
36
  return [
34
37
  { path: '/tax-rates', label: 'Tax rates' },
@@ -46,7 +49,8 @@ function TaxRateDetailPage() {
46
49
  const { i18n } = useLingui();
47
50
 
48
51
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
49
- queryDocument: taxRateDetailQuery,
52
+ pageId,
53
+ queryDocument: taxRateDetailDocument,
50
54
  createDocument: createTaxRateDocument,
51
55
  updateDocument: updateTaxRateDocument,
52
56
  setValuesForUpdate: entity => {
@@ -77,7 +81,7 @@ function TaxRateDetailPage() {
77
81
  });
78
82
 
79
83
  return (
80
- <Page pageId="tax-rate-detail" form={form} submitHandler={submitHandler} entity={entity}>
84
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
81
85
  <PageTitle>{creatingNewEntity ? <Trans>New tax rate</Trans> : (entity?.name ?? '')}</PageTitle>
82
86
  <PageActionBar>
83
87
  <PageActionBarRight>
@@ -42,7 +42,7 @@ export const zoneMembersQuery = graphql(`
42
42
  }
43
43
  `);
44
44
 
45
- export const zoneDetailQuery = graphql(
45
+ export const zoneDetailDocument = graphql(
46
46
  `
47
47
  query ZoneDetail($id: ID!) {
48
48
  zone(id: $id) {
@@ -20,12 +20,15 @@ import { Trans, useLingui } from '@/lib/trans.js';
20
20
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
21
21
  import { toast } from 'sonner';
22
22
  import { ZoneCountriesTable } from './components/zone-countries-table.js';
23
- import { createZoneDocument, updateZoneDocument, zoneDetailQuery } from './zones.graphql.js';
23
+ import { createZoneDocument, updateZoneDocument, zoneDetailDocument } from './zones.graphql.js';
24
+
25
+ const pageId = 'zone-detail';
24
26
 
25
27
  export const Route = createFileRoute('/_authenticated/_zones/zones_/$id')({
26
28
  component: ZoneDetailPage,
27
29
  loader: detailPageRouteLoader({
28
- queryDocument: zoneDetailQuery,
30
+ pageId,
31
+ queryDocument: zoneDetailDocument,
29
32
  breadcrumb(isNew, entity) {
30
33
  return [{ path: '/zones', label: 'Zones' }, isNew ? <Trans>New zone</Trans> : entity?.name];
31
34
  },
@@ -40,7 +43,8 @@ function ZoneDetailPage() {
40
43
  const { i18n } = useLingui();
41
44
 
42
45
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
43
- queryDocument: zoneDetailQuery,
46
+ pageId,
47
+ queryDocument: zoneDetailDocument,
44
48
  createDocument: createZoneDocument,
45
49
  updateDocument: updateZoneDocument,
46
50
  setValuesForUpdate: entity => {
@@ -66,7 +70,7 @@ function ZoneDetailPage() {
66
70
  });
67
71
 
68
72
  return (
69
- <Page pageId="zone-detail" form={form} submitHandler={submitHandler} entity={entity}>
73
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
70
74
  <PageTitle>{creatingNewEntity ? <Trans>New zone</Trans> : (entity?.name ?? '')}</PageTitle>
71
75
  <PageActionBar>
72
76
  <PageActionBarRight>
@@ -41,7 +41,12 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
41
41
  const customFields = useCustomFieldConfig(entityType);
42
42
 
43
43
  const getFieldName = (fieldDef: CustomFieldConfig) => {
44
- const name = fieldDef.type === 'relation' ? fieldDef.name + 'Id' : fieldDef.name;
44
+ const name =
45
+ fieldDef.type === 'relation'
46
+ ? fieldDef.list
47
+ ? fieldDef.name + 'Ids'
48
+ : fieldDef.name + 'Id'
49
+ : fieldDef.name;
45
50
  return formPathPrefix ? `${formPathPrefix}.customFields.${name}` : `customFields.${name}`;
46
51
  };
47
52
 
@@ -266,6 +271,18 @@ function FormInputForType({
266
271
  );
267
272
  case 'boolean':
268
273
  return <Switch checked={field.value} onCheckedChange={field.onChange} disabled={isReadonly} />;
274
+ case 'relation':
275
+ if (fieldDef.list) {
276
+ return (
277
+ <Input
278
+ {...field}
279
+ onChange={e => field.onChange(e.target.value.split(','))}
280
+ disabled={isReadonly}
281
+ />
282
+ );
283
+ } else {
284
+ return <Input {...field} disabled={isReadonly} />;
285
+ }
269
286
  default:
270
287
  return <Input {...field} disabled={isReadonly} />;
271
288
  }