@wilshop/dashboard 3.5.6 → 3.5.7

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 (133) hide show
  1. package/dist/plugin/dashboard.plugin.d.ts +1 -1
  2. package/dist/plugin/dashboard.plugin.js +1 -1
  3. package/dist/vite/utils/compiler.js +50 -24
  4. package/dist/vite/utils/path-transformer.d.ts +20 -0
  5. package/dist/vite/utils/path-transformer.js +116 -0
  6. package/dist/vite/utils/plugin-discovery.js +3 -2
  7. package/dist/vite/utils/ui-config.js +15 -1
  8. package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
  9. package/dist/vite/vite-plugin-lingui-babel.js +90 -8
  10. package/dist/vite/vite-plugin-translations.js +2 -2
  11. package/dist/vite/vite-plugin-ui-config.d.ts +31 -0
  12. package/package.json +10 -6
  13. package/src/app/common/delete-bulk-action.tsx +1 -1
  14. package/src/app/common/duplicate-bulk-action.tsx +1 -1
  15. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -3
  16. package/src/app/routes/_authenticated/_collections/collections.tsx +169 -48
  17. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +36 -5
  18. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +1 -1
  19. package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +7 -1
  20. package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +31 -29
  21. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
  22. package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
  23. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
  24. package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
  25. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +8 -5
  26. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +79 -54
  27. package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +43 -3
  28. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +19 -3
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +1 -0
  30. package/src/app/routes/_authenticated/_orders/components/refund-order-dialog.tsx +372 -0
  31. package/src/app/routes/_authenticated/_orders/hooks/use-refund-order.ts +345 -0
  32. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +41 -0
  33. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
  34. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +51 -0
  35. package/src/app/routes/_authenticated/_orders/utils/refund-utils.ts +100 -0
  36. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +1 -1
  37. package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +4 -1
  38. package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +7 -1
  39. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +18 -2
  40. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +1 -1
  41. package/src/app/routes/_authenticated/_product-variants/components/variant-price-detail.tsx +6 -2
  42. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
  43. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
  44. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +1 -1
  45. package/src/app/routes/_authenticated/_profile/profile.graphql.ts +7 -0
  46. package/src/app/routes/_authenticated/_profile/profile.tsx +25 -1
  47. package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +7 -1
  48. package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +7 -1
  49. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +18 -2
  50. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +7 -1
  51. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +4 -1
  52. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +14 -2
  53. package/src/i18n/common-strings.ts +7 -0
  54. package/src/i18n/locales/ar.po +669 -399
  55. package/src/i18n/locales/bg.po +1889 -46
  56. package/src/i18n/locales/cs.po +676 -406
  57. package/src/i18n/locales/de.po +676 -406
  58. package/src/i18n/locales/en.po +669 -399
  59. package/src/i18n/locales/es.po +676 -406
  60. package/src/i18n/locales/fa.po +676 -406
  61. package/src/i18n/locales/fr.po +676 -406
  62. package/src/i18n/locales/he.po +676 -406
  63. package/src/i18n/locales/hr.po +676 -406
  64. package/src/i18n/locales/it.po +676 -406
  65. package/src/i18n/locales/ja.po +676 -406
  66. package/src/i18n/locales/nb.po +676 -406
  67. package/src/i18n/locales/ne.po +676 -406
  68. package/src/i18n/locales/pl.po +676 -406
  69. package/src/i18n/locales/pt_BR.po +676 -406
  70. package/src/i18n/locales/pt_PT.po +676 -406
  71. package/src/i18n/locales/ru.po +676 -406
  72. package/src/i18n/locales/sv.po +676 -406
  73. package/src/i18n/locales/tr.po +676 -406
  74. package/src/i18n/locales/uk.po +676 -406
  75. package/src/i18n/locales/zh_Hans.po +676 -406
  76. package/src/i18n/locales/zh_Hant.po +676 -406
  77. package/src/lib/components/data-input/facet-value-input.tsx +2 -2
  78. package/src/lib/components/data-input/index.ts +1 -0
  79. package/src/lib/components/data-input/select-with-options.tsx +23 -7
  80. package/src/lib/components/data-input/struct-form-input.tsx +53 -21
  81. package/src/lib/components/data-input/text-input.tsx +1 -1
  82. package/src/lib/components/data-table/data-table-bulk-actions.tsx +2 -1
  83. package/src/lib/components/data-table/data-table-context.tsx +2 -10
  84. package/src/lib/components/data-table/data-table-utils.ts +34 -12
  85. package/src/lib/components/data-table/data-table.tsx +68 -30
  86. package/src/lib/components/data-table/global-views-bar.tsx +1 -1
  87. package/src/lib/components/data-table/my-views-button.tsx +1 -1
  88. package/src/lib/components/data-table/save-view-button.tsx +1 -1
  89. package/src/lib/components/data-table/use-generated-columns.tsx +9 -2
  90. package/src/lib/components/data-table/views-sheet.tsx +1 -1
  91. package/src/lib/components/layout/channel-switcher.tsx +16 -17
  92. package/src/lib/components/layout/manage-languages-dialog.tsx +1 -1
  93. package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +1 -1
  94. package/src/lib/components/shared/configurable-operation-input.tsx +23 -0
  95. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +45 -0
  96. package/src/lib/components/shared/configurable-operation-selector.tsx +5 -0
  97. package/src/lib/components/shared/paginated-list-context.ts +10 -0
  98. package/src/lib/components/shared/paginated-list-data-table.tsx +6 -32
  99. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +1 -1
  100. package/src/lib/components/ui/alert.tsx +2 -0
  101. package/src/lib/constants.ts +7 -319
  102. package/src/lib/framework/dashboard-widget/base-widget.tsx +3 -12
  103. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +1 -1
  104. package/src/lib/framework/dashboard-widget/metrics-widget/chart.tsx +1 -1
  105. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +1 -1
  106. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +1 -1
  107. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +2 -20
  108. package/src/lib/framework/extension-api/input-component-extensions.tsx +4 -0
  109. package/src/lib/framework/form-engine/custom-form-component.tsx +13 -3
  110. package/src/lib/framework/form-engine/form-engine-types.ts +3 -5
  111. package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
  112. package/src/lib/framework/form-engine/use-generated-form.tsx +6 -2
  113. package/src/lib/framework/form-engine/utils.spec.ts +129 -2
  114. package/src/lib/framework/form-engine/utils.ts +36 -9
  115. package/src/lib/framework/form-engine/value-transformers.ts +6 -0
  116. package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
  117. package/src/lib/framework/page/detail-page.tsx +22 -37
  118. package/src/lib/framework/page/list-page.stories.tsx +41 -2
  119. package/src/lib/framework/page/list-page.tsx +8 -0
  120. package/src/lib/graphql/graphql-env.d.ts +33 -16
  121. package/src/lib/graphql/schema-enums.ts +13 -0
  122. package/src/lib/hooks/use-alerts-context.ts +10 -0
  123. package/src/lib/hooks/use-alerts.ts +1 -1
  124. package/src/lib/hooks/use-data-table-context.ts +11 -0
  125. package/src/lib/hooks/use-dynamic-translations.ts +7 -0
  126. package/src/lib/hooks/use-job-queue-polling.ts +160 -0
  127. package/src/lib/hooks/use-paginated-list.ts +28 -0
  128. package/src/lib/hooks/use-widget-dimensions.ts +12 -0
  129. package/src/lib/hooks/use-widget-filters.ts +21 -0
  130. package/src/lib/index.ts +12 -0
  131. package/src/lib/providers/alerts-provider.tsx +3 -11
  132. package/src/lib/virtual.d.ts +5 -0
  133. package/src/lib/utils/global-languages.ts +0 -268
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useLingui } from '@lingui/react/macro';
4
- import { createContext, PropsWithChildren, useContext } from 'react';
3
+ import { createContext, PropsWithChildren } from 'react';
5
4
 
6
5
  export interface DefinedDateRange {
7
6
  from: Date;
@@ -12,25 +11,8 @@ export interface WidgetFilters {
12
11
  dateRange: DefinedDateRange;
13
12
  }
14
13
 
15
- const WidgetFiltersContext = createContext<WidgetFilters | undefined>(undefined);
14
+ export const WidgetFiltersContext = createContext<WidgetFilters | undefined>(undefined);
16
15
 
17
16
  export function WidgetFiltersProvider({ children, filters }: PropsWithChildren<{ filters: WidgetFilters }>) {
18
17
  return <WidgetFiltersContext.Provider value={filters}>{children}</WidgetFiltersContext.Provider>;
19
18
  }
20
-
21
- /**
22
- * @description
23
- * Exposes a context object for use in building Insights page widgets.
24
- *
25
- * @docsCategory hooks
26
- * @docsPage useWidgetFilters
27
- * @since 3.5.0
28
- */
29
- export function useWidgetFilters() {
30
- const { t } = useLingui();
31
- const context = useContext(WidgetFiltersContext);
32
- if (context === undefined) {
33
- throw new Error(t`useWidgetFilters must be used within a WidgetFiltersProvider`);
34
- }
35
- return context;
36
- }
@@ -4,11 +4,13 @@ import {
4
4
  CustomerGroupInput,
5
5
  FacetValueInput,
6
6
  MoneyInput,
7
+ NumberInput,
7
8
  ProductMultiInput,
8
9
  RichTextInput,
9
10
  SelectWithOptions,
10
11
  } from '@/vdb/components/data-input/index.js';
11
12
  import { PasswordFormInput } from '@/vdb/components/data-input/password-form-input.js';
13
+ import { TextInput } from '@/vdb/components/data-input/text-input.js';
12
14
  import { TextareaInput } from '@/vdb/components/data-input/textarea-input.js';
13
15
  import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
14
16
  import { globalRegistry } from '../registry/global-registry.js';
@@ -38,6 +40,8 @@ inputComponents.set('relation-form-input', DefaultRelationInput);
38
40
  inputComponents.set('select-form-input', SelectWithOptions);
39
41
  inputComponents.set('product-multi-form-input', ProductMultiInput);
40
42
  inputComponents.set('combination-mode-form-input', CombinationModeInput);
43
+ inputComponents.set('number-form-input', NumberInput);
44
+ inputComponents.set('text-form-input', TextInput);
41
45
 
42
46
  export function getInputComponent(id: string | undefined): DashboardFormComponent | undefined {
43
47
  if (!id) {
@@ -1,15 +1,25 @@
1
1
  import { getInputComponent } from '@/vdb/framework/extension-api/input-component-extensions.js';
2
-
2
+ import { DefaultInputForType } from '@/vdb/framework/form-engine/default-input-for-type.js';
3
3
  import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
4
4
 
5
+ const warnedComponents = new Set<string>();
6
+
5
7
  export function CustomFormComponent(props: DashboardFormComponentProps) {
6
8
  if (!props.fieldDef) {
7
9
  return null;
8
10
  }
9
- const Component = getInputComponent(props.fieldDef.ui?.component);
11
+ const componentId = props.fieldDef.ui?.component;
12
+ const Component = getInputComponent(componentId);
10
13
 
11
14
  if (!Component) {
12
- return null;
15
+ if (componentId && !warnedComponents.has(componentId)) {
16
+ warnedComponents.add(componentId);
17
+ console.warn(
18
+ `Custom form component "${componentId}" not found for field "${props.fieldDef.name}". ` +
19
+ `Falling back to default input for type "${props.fieldDef.type}".`,
20
+ );
21
+ }
22
+ return <DefaultInputForType {...props} />;
13
23
  }
14
24
 
15
25
  return <Component {...props} />;
@@ -110,11 +110,9 @@ export type DashboardFormComponentProps<
110
110
  * // implementation omitted
111
111
  * }
112
112
  *
113
- * // highlight-start
114
- * MyCustomInput.metadata = {
115
- * isListInput: true
116
- * }
117
- * // highlight-end
113
+ * MyCustomInput.metadata = { // [!code highlight]
114
+ * isListInput: true // [!code highlight]
115
+ * } // [!code highlight]
118
116
  * ```
119
117
  *
120
118
  * @docsCategory extensions-api
@@ -309,7 +309,10 @@ export function createFormSchemaFromFields(
309
309
  customFieldConfigs && customFieldConfigs.length > 0
310
310
  ? processCustomFieldsSchema(customFieldConfigs, isTranslationContext)
311
311
  : {};
312
- schemaConfig[field.name] = z.object(customFieldsSchema).optional();
312
+ // Use .passthrough() to preserve custom field values that aren't in the schema
313
+ // This is essential for nested entities (e.g., ProductVariantPrice) whose custom
314
+ // field configs aren't passed to the parent form schema builder
315
+ schemaConfig[field.name] = z.object(customFieldsSchema).passthrough().optional();
313
316
  } else if (field.typeInfo) {
314
317
  const isNestedTranslationContext = field.name === 'translations' || isTranslationContext;
315
318
  let nestedType: ZodType = createFormSchemaFromFields(
@@ -98,9 +98,13 @@ export function useGeneratedForm<
98
98
  const defaultValues = getDefaultValuesFromFields(updateFields, activeChannel?.defaultLanguageCode);
99
99
  const processedEntity = ensureTranslationsForAllLanguages(entity, availableLanguages, defaultValues);
100
100
 
101
+ // Also ensure defaultValues has translations for all languages (for creation case)
102
+ const processedDefaultValues =
103
+ ensureTranslationsForAllLanguages(defaultValues, availableLanguages, defaultValues) ?? defaultValues;
104
+
101
105
  const values = processedEntity
102
106
  ? transformRelationFields(updateFields, setValues(processedEntity))
103
- : defaultValues;
107
+ : processedDefaultValues;
104
108
 
105
109
  const form = useForm({
106
110
  resolver: async (values, context, options) => {
@@ -111,7 +115,7 @@ export function useGeneratedForm<
111
115
  return result;
112
116
  },
113
117
  mode: 'onChange',
114
- defaultValues,
118
+ defaultValues: processedDefaultValues,
115
119
  values,
116
120
  });
117
121
  let submitHandler = (event: FormEvent): any => {
@@ -1,9 +1,9 @@
1
1
  import { graphql, VariablesOf } from 'gql.tada';
2
2
  import { describe, expect, it } from 'vitest';
3
3
 
4
- import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
4
+ import { FieldInfo, getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
5
5
 
6
- import { removeEmptyIdFields } from './utils.js';
6
+ import { removeEmptyIdFields, transformRelationFields } from './utils.js';
7
7
 
8
8
  const createProductDocument = graphql(`
9
9
  mutation CreateProduct($input: CreateProductInput!) {
@@ -35,3 +35,130 @@ describe('removeEmptyIdFields', () => {
35
35
  expect(result).toEqual({ input: { translations: [] } });
36
36
  });
37
37
  });
38
+
39
+ describe('transformRelationFields', () => {
40
+ const createFieldsWithListRelation = (): FieldInfo[] => [
41
+ {
42
+ name: 'customFields',
43
+ type: 'CustomFields',
44
+ nullable: true,
45
+ list: false,
46
+ isPaginatedList: false,
47
+ isScalar: false,
48
+ typeInfo: [
49
+ {
50
+ name: 'featuredProductsIds',
51
+ type: 'ID',
52
+ nullable: true,
53
+ list: true,
54
+ isPaginatedList: false,
55
+ isScalar: true,
56
+ },
57
+ ],
58
+ },
59
+ ];
60
+
61
+ const createFieldsWithSingleRelation = (): FieldInfo[] => [
62
+ {
63
+ name: 'customFields',
64
+ type: 'CustomFields',
65
+ nullable: true,
66
+ list: false,
67
+ isPaginatedList: false,
68
+ isScalar: false,
69
+ typeInfo: [
70
+ {
71
+ name: 'featuredProductId',
72
+ type: 'ID',
73
+ nullable: true,
74
+ list: false,
75
+ isPaginatedList: false,
76
+ isScalar: true,
77
+ },
78
+ ],
79
+ },
80
+ ];
81
+
82
+ it('should extract IDs from list relation and delete original field', () => {
83
+ const entity = {
84
+ customFields: {
85
+ featuredProducts: [
86
+ { id: '1', name: 'Product 1' },
87
+ { id: '2', name: 'Product 2' },
88
+ ],
89
+ },
90
+ };
91
+ const result = transformRelationFields(createFieldsWithListRelation(), entity);
92
+
93
+ expect(result.customFields).toEqual({ featuredProductsIds: ['1', '2'] });
94
+ expect(result.customFields).not.toHaveProperty('featuredProducts');
95
+ });
96
+
97
+ it('should handle empty array for clearing list relations', () => {
98
+ const entity = { customFields: { featuredProducts: [] } };
99
+ const result = transformRelationFields(createFieldsWithListRelation(), entity);
100
+
101
+ expect(result.customFields).toEqual({ featuredProductsIds: [] });
102
+ });
103
+
104
+ it('should handle undefined/null list relation by deleting field', () => {
105
+ const undefinedResult = transformRelationFields(createFieldsWithListRelation(), { customFields: {} });
106
+ const nullResult = transformRelationFields(createFieldsWithListRelation(), {
107
+ customFields: { featuredProducts: null },
108
+ });
109
+
110
+ expect(undefinedResult.customFields).not.toHaveProperty('featuredProductsIds');
111
+ expect(nullResult.customFields).not.toHaveProperty('featuredProducts');
112
+ });
113
+
114
+ it('should extract ID from single relation and delete original field', () => {
115
+ const entity = { customFields: { featuredProduct: { id: '1', name: 'Product 1' } } };
116
+ const result = transformRelationFields(createFieldsWithSingleRelation(), entity);
117
+
118
+ expect(result.customFields).toEqual({ featuredProductId: '1' });
119
+ expect(result.customFields).not.toHaveProperty('featuredProduct');
120
+ });
121
+
122
+ it('should not mutate the original entity', () => {
123
+ const entity = { customFields: { featuredProducts: [{ id: '1' }] } };
124
+ const result = transformRelationFields(createFieldsWithListRelation(), entity);
125
+
126
+ expect(entity.customFields.featuredProducts).toEqual([{ id: '1' }]);
127
+ expect(result).not.toBe(entity);
128
+ });
129
+
130
+ it('should preserve other custom fields while transforming relations', () => {
131
+ const fields: FieldInfo[] = [
132
+ {
133
+ name: 'customFields',
134
+ type: 'CustomFields',
135
+ nullable: true,
136
+ list: false,
137
+ isPaginatedList: false,
138
+ isScalar: false,
139
+ typeInfo: [
140
+ {
141
+ name: 'featuredProductsIds',
142
+ type: 'ID',
143
+ nullable: true,
144
+ list: true,
145
+ isPaginatedList: false,
146
+ isScalar: true,
147
+ },
148
+ {
149
+ name: 'notes',
150
+ type: 'String',
151
+ nullable: true,
152
+ list: false,
153
+ isPaginatedList: false,
154
+ isScalar: true,
155
+ },
156
+ ],
157
+ },
158
+ ];
159
+ const entity = { customFields: { featuredProducts: [{ id: '1' }], notes: 'Some notes' } };
160
+ const result = transformRelationFields(fields, entity);
161
+
162
+ expect(result.customFields).toEqual({ featuredProductsIds: ['1'], notes: 'Some notes' });
163
+ });
164
+ });
@@ -58,12 +58,10 @@ export function transformRelationFields<E extends Record<string, any>>(fields: F
58
58
  const propertyAccessorKey = customField.name.replace(/Ids$/, '');
59
59
  const relationValue = entity.customFields[propertyAccessorKey];
60
60
 
61
- if (relationValue) {
62
- const relationIdValue = relationValue.map((v: { id: string }) => v.id);
63
- if (relationIdValue && relationIdValue.length > 0) {
64
- processedEntity.customFields[relationField] = relationIdValue;
65
- }
61
+ if (Array.isArray(relationValue)) {
62
+ processedEntity.customFields[relationField] = relationValue.map((v: { id: string }) => v.id);
66
63
  }
64
+ delete processedEntity.customFields[propertyAccessorKey];
67
65
  } else {
68
66
  // For single fields, the accessor is the field name without the "Id" suffix
69
67
  const propertyAccessorKey = customField.name.replace(/Id$/, '');
@@ -253,14 +251,43 @@ export function isStringStructField(input: StructField): input is StringStructFi
253
251
  }
254
252
 
255
253
  /**
256
- * String struct field that has options (select dropdown)
254
+ * String struct field that has options (select dropdown).
255
+ * Checks for options defined either directly or via ui.options.
257
256
  */
258
257
  export function isStringStructFieldWithOptions(
259
258
  input: StructField,
260
259
  ): input is StringStructField & { options: any[] } {
261
- return (
262
- input.type === 'string' && input.hasOwnProperty('options') && Array.isArray((input as any).options)
263
- );
260
+ if (input.type !== 'string') {
261
+ return false;
262
+ }
263
+ // Check for direct options property
264
+ if (input.hasOwnProperty('options') && Array.isArray((input as any).options)) {
265
+ return true;
266
+ }
267
+ // Also check for ui.options (fallback pattern)
268
+ if (Array.isArray((input as any).ui?.options)) {
269
+ return true;
270
+ }
271
+ return false;
272
+ }
273
+
274
+ /**
275
+ * Extracts options from a field definition, normalizing the different locations
276
+ * where options can be defined (direct property or ui.options).
277
+ * Works for both ConfigurableFieldDef and StructField types.
278
+ */
279
+ export function extractFieldOptions(
280
+ field: ConfigurableFieldDef | StructField,
281
+ ): NonNullable<StringCustomFieldConfig['options']> {
282
+ // Check direct options property first
283
+ if ((field as any).options && Array.isArray((field as any).options)) {
284
+ return (field as any).options;
285
+ }
286
+ // Fall back to ui.options
287
+ if (field.ui?.options && Array.isArray(field.ui.options)) {
288
+ return field.ui.options;
289
+ }
290
+ return [];
264
291
  }
265
292
 
266
293
  /**
@@ -40,6 +40,12 @@ export const jsonStringValueTransformer: ValueTransformer = {
40
40
  return value;
41
41
  }
42
42
 
43
+ // For scalar string fields, return the raw value without JSON parsing.
44
+ // This prevents issues like "0" being parsed as number 0, or "-0" becoming -0 which is "0" in the input.
45
+ if (fieldDef.type === 'string' && !fieldDef.list) {
46
+ return value;
47
+ }
48
+
43
49
  try {
44
50
  // For JSON string mode, parse the string to get the native value
45
51
  const parsed = JSON.parse(value);
@@ -16,7 +16,7 @@ export interface DetailPageRouteLoaderConfig<T extends TypedDocumentNode<any, an
16
16
  * the detail query document) get correctly applied at the route loader level.
17
17
  */
18
18
  pageId?: string;
19
- queryDocument: T;
19
+ queryDocument: T | (() => T);
20
20
  breadcrumb: (
21
21
  isNew: boolean,
22
22
  entity: DetailEntity<T>,
@@ -38,12 +38,14 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
38
38
  params: any;
39
39
  location: ParsedLocation;
40
40
  }) => {
41
+ const resolvedQueryDocument = typeof queryDocument === 'function' ? queryDocument() : queryDocument;
42
+
41
43
  if (!params.id) {
42
44
  throw new Error('ID param is required');
43
45
  }
44
46
  const isNew = params.id === NEW_ENTITY_PATH;
45
47
  const { extendedQuery: extendedQueryDocument } = extendDetailFormQuery(
46
- addCustomFields(queryDocument),
48
+ addCustomFields(resolvedQueryDocument),
47
49
  pageId,
48
50
  );
49
51
  const result = isNew
@@ -53,8 +55,8 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
53
55
  { id: params.id },
54
56
  );
55
57
 
56
- const entityField = getQueryName(queryDocument);
57
- const entityName = getQueryTypeFieldInfo(queryDocument)?.type;
58
+ const entityField = getQueryName(resolvedQueryDocument);
59
+ const entityName = getQueryTypeFieldInfo(resolvedQueryDocument)?.type;
58
60
 
59
61
  if (!isNew && !result[entityField]) {
60
62
  throw new Error(`${entityName} with the ID ${params.id} was not found`);
@@ -1,25 +1,20 @@
1
- import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
2
1
  import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
2
+ import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
3
3
  import { Button } from '@/vdb/components/ui/button.js';
4
- import { Checkbox } from '@/vdb/components/ui/checkbox.js';
5
- import { Input } from '@/vdb/components/ui/input.js';
6
4
  import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
5
+ import { DefaultInputForType } from '@/vdb/framework/form-engine/default-input-for-type.js';
7
6
  import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
8
7
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
9
8
  import { Trans } from '@lingui/react/macro';
10
9
  import { AnyRoute, useNavigate } from '@tanstack/react-router';
11
10
  import { ResultOf, VariablesOf } from 'gql.tada';
12
11
  import { toast } from 'sonner';
12
+ import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
13
13
  import {
14
14
  FieldInfo,
15
15
  getEntityName,
16
16
  getOperationVariablesFields,
17
17
  } from '../document-introspection/get-document-structure.js';
18
-
19
- import { NumberInput } from '@/vdb/components/data-input/number-input.js';
20
- import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
21
- import { FormControl } from '@/vdb/components/ui/form.js';
22
- import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
23
18
  import {
24
19
  CustomFieldsPageBlock,
25
20
  DetailFormGrid,
@@ -97,6 +92,18 @@ export interface DetailPageFieldProps<
97
92
  field: ControllerRenderProps<TFieldValues, TName>;
98
93
  }
99
94
 
95
+ /**
96
+ * Maps GraphQL schema types (PascalCase) to form engine types (lowercase)
97
+ */
98
+ const graphqlTypeMap: Record<string, string> = {
99
+ Int: 'int',
100
+ Float: 'float',
101
+ Boolean: 'boolean',
102
+ DateTime: 'datetime',
103
+ String: 'string',
104
+ ID: 'string',
105
+ };
106
+
100
107
  /**
101
108
  * Renders form input components based on field type
102
109
  */
@@ -104,33 +111,13 @@ function FieldInputRenderer<
104
111
  TFieldValues extends FieldValues = FieldValues,
105
112
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
106
113
  >({ fieldInfo, field }: DetailPageFieldProps<TFieldValues, TName>) {
107
- switch (fieldInfo.type) {
108
- case 'Int':
109
- case 'Float':
110
- return (
111
- <FormControl>
112
- <NumberInput {...field} />
113
- </FormControl>
114
- );
115
- case 'DateTime':
116
- return (
117
- <FormControl>
118
- <DateTimeInput {...field} />
119
- </FormControl>
120
- );
121
- case 'Boolean':
122
- return (
123
- <FormControl>
124
- <Checkbox value={field.value} onCheckedChange={field.onChange} />
125
- </FormControl>
126
- );
127
- default:
128
- return (
129
- <FormControl>
130
- <Input {...field} />
131
- </FormControl>
132
- );
133
- }
114
+ const type = graphqlTypeMap[fieldInfo.type] ?? 'string';
115
+ return (
116
+ <DefaultInputForType
117
+ {...field}
118
+ fieldDef={{ type, name: fieldInfo.name } as any}
119
+ />
120
+ );
134
121
  }
135
122
 
136
123
  /**
@@ -219,7 +206,6 @@ export function DetailPage<
219
206
  control={form.control}
220
207
  name={fieldInfo.name as never}
221
208
  label={fieldInfo.name}
222
- renderFormControl={false}
223
209
  render={({ field }) => (
224
210
  <FieldInputRenderer fieldInfo={fieldInfo} field={field} />
225
211
  )}
@@ -237,7 +223,6 @@ export function DetailPage<
237
223
  control={form.control}
238
224
  name={fieldInfo.name as never}
239
225
  label={fieldInfo.name}
240
- renderFormControl={false}
241
226
  render={({ field }) => (
242
227
  <FieldInputRenderer fieldInfo={fieldInfo} field={field} />
243
228
  )}
@@ -5,6 +5,7 @@ import { ListPage, ListPageProps } from '@/vdb/framework/page/list-page.js';
5
5
  import { graphql } from '@/vdb/graphql/graphql.js';
6
6
  import type { Meta, StoryObj } from '@storybook/react';
7
7
  import { PlusIcon } from 'lucide-react';
8
+ import { expect } from 'storybook/test';
8
9
 
9
10
  import { DemoRouterProvider } from '../../../../.storybook/providers.js';
10
11
 
@@ -72,8 +73,9 @@ const meta = {
72
73
  description: 'GraphQL mutation document for deleting items',
73
74
  },
74
75
  customizeColumns: {
75
- control: false,
76
- description: 'Customize column rendering and behavior',
76
+ control: 'object',
77
+ description:
78
+ 'Customize column rendering and behavior. Use `meta.disabled: true` to exclude columns from the table.',
77
79
  },
78
80
  defaultVisibility: {
79
81
  control: 'object',
@@ -99,6 +101,11 @@ export const BasicList: Story = {
99
101
  code: true,
100
102
  enabled: true,
101
103
  },
104
+ customizeColumns: {
105
+ updatedAt: {
106
+ meta: { disabled: true },
107
+ },
108
+ },
102
109
  },
103
110
  };
104
111
 
@@ -166,6 +173,38 @@ export const WithSearch: Story = {
166
173
  },
167
174
  };
168
175
 
176
+ /**
177
+ * ListPage with disabled columns.
178
+ * Shows how to use `meta.disabled` to completely exclude columns from the table
179
+ * and the column visibility toggle. The disabled columns' data can still be
180
+ * accessed in other column renderers.
181
+ */
182
+ export const WithDisabledColumns: Story = {
183
+ args: {
184
+ pageId: 'country-list-disabled',
185
+ listQuery: countriesListQuery,
186
+ title: 'Countries',
187
+ defaultVisibility: {
188
+ name: true,
189
+ code: true,
190
+ enabled: true,
191
+ },
192
+ customizeColumns: {
193
+ name: {
194
+ cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
195
+ },
196
+ // The createdAt and updatedAt columns are disabled and won't appear
197
+ // in the table or in the column visibility toggle
198
+ updatedAt: {
199
+ meta: { disabled: true },
200
+ },
201
+ createdAt: {
202
+ meta: { disabled: true },
203
+ },
204
+ },
205
+ },
206
+ };
207
+
169
208
  /**
170
209
  * Complete example with action bar and all features.
171
210
  * Shows the full ListPage configuration including custom action buttons.
@@ -154,6 +154,7 @@ export interface ListPageProps<
154
154
  * };
155
155
  * }}
156
156
  * />
157
+ * ```
157
158
  * @param searchTerm
158
159
  */
159
160
  onSearchTermChange?: (searchTerm: string) => NonNullable<V['options']>['filter'];
@@ -208,6 +209,13 @@ export interface ListPageProps<
208
209
  * );
209
210
  * },
210
211
  * },
212
+ * // Use `meta.disabled` to completely exclude a column from the table,
213
+ * // including the column visibility toggle. This is useful when you need
214
+ * // to fetch certain fields for use in custom cell renderers, but don't
215
+ * // want those fields to appear as their own columns.
216
+ * productVariantCount: {
217
+ * meta: { disabled: true },
218
+ * },
211
219
  * ```
212
220
  */
213
221
  customizeColumns?: CustomizeColumnConfig<T>;