@vendure/dashboard 3.4.1-master-202508210231 → 3.4.1

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 (58) hide show
  1. package/package.json +152 -152
  2. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +2 -1
  3. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +1 -0
  4. package/src/lib/components/data-input/affixed-input.tsx +19 -5
  5. package/src/lib/components/data-input/boolean-input.tsx +9 -0
  6. package/src/lib/components/data-input/checkbox-input.tsx +8 -0
  7. package/src/lib/components/data-input/combination-mode-input.tsx +11 -2
  8. package/src/lib/components/data-input/configurable-operation-list-input.tsx +26 -401
  9. package/src/lib/components/data-input/custom-field-list-input.tsx +18 -25
  10. package/src/lib/components/data-input/customer-group-input.tsx +7 -11
  11. package/src/lib/components/data-input/datetime-input.tsx +9 -13
  12. package/src/lib/components/data-input/default-relation-input.tsx +29 -9
  13. package/src/lib/components/data-input/facet-value-input.tsx +15 -13
  14. package/src/lib/components/data-input/index.ts +2 -2
  15. package/src/lib/components/data-input/money-input.tsx +27 -20
  16. package/src/lib/components/data-input/number-input.tsx +48 -0
  17. package/src/lib/components/data-input/password-input.tsx +16 -0
  18. package/src/lib/components/data-input/{product-multi-selector.tsx → product-multi-selector-input.tsx} +8 -15
  19. package/src/lib/components/data-input/relation-input.tsx +7 -6
  20. package/src/lib/components/data-input/rich-text-input.tsx +10 -13
  21. package/src/lib/components/data-input/select-with-options.tsx +29 -17
  22. package/src/lib/components/data-input/struct-form-input.tsx +54 -59
  23. package/src/lib/components/data-input/text-input.tsx +9 -0
  24. package/src/lib/components/data-input/textarea-input.tsx +16 -0
  25. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +3 -0
  26. package/src/lib/components/data-table/use-generated-columns.tsx +16 -5
  27. package/src/lib/components/shared/configurable-operation-arg-input.tsx +3 -10
  28. package/src/lib/components/shared/configurable-operation-input.tsx +1 -6
  29. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +8 -5
  30. package/src/lib/components/shared/configurable-operation-selector.tsx +5 -5
  31. package/src/lib/components/shared/custom-fields-form.tsx +20 -49
  32. package/src/lib/components/shared/multi-select.tsx +1 -1
  33. package/src/lib/framework/component-registry/component-registry.tsx +9 -32
  34. package/src/lib/framework/component-registry/display-component.tsx +28 -0
  35. package/src/lib/framework/extension-api/display-component-extensions.tsx +0 -14
  36. package/src/lib/framework/extension-api/input-component-extensions.tsx +52 -34
  37. package/src/lib/framework/extension-api/logic/data-table.ts +4 -27
  38. package/src/lib/framework/extension-api/logic/form-components.ts +3 -2
  39. package/src/lib/framework/extension-api/types/detail-forms.ts +2 -38
  40. package/src/lib/framework/extension-api/types/form-components.ts +2 -4
  41. package/src/lib/framework/form-engine/custom-form-component-extensions.ts +0 -23
  42. package/src/lib/framework/form-engine/custom-form-component.tsx +8 -25
  43. package/src/lib/framework/form-engine/default-input-for-type.tsx +35 -0
  44. package/src/lib/framework/form-engine/form-control-adapter.tsx +192 -0
  45. package/src/lib/framework/form-engine/form-engine-types.ts +163 -0
  46. package/src/lib/framework/form-engine/form-schema-tools.ts +55 -71
  47. package/src/lib/framework/form-engine/overridden-form-component.tsx +2 -2
  48. package/src/lib/framework/form-engine/utils.ts +223 -0
  49. package/src/lib/{components/shared → framework/form-engine}/value-transformers.ts +9 -9
  50. package/src/lib/framework/registry/registry-types.ts +3 -5
  51. package/src/lib/graphql/graphql-env.d.ts +11 -7
  52. package/src/lib/index.ts +28 -1
  53. package/src/lib/providers/server-config.tsx +1 -0
  54. package/src/lib/components/shared/direct-form-component-map.tsx +0 -393
  55. package/src/lib/components/shared/universal-field-definition.ts +0 -118
  56. package/src/lib/components/shared/universal-form-input.tsx +0 -175
  57. package/src/lib/components/shared/universal-input-components.tsx +0 -291
  58. package/src/lib/framework/component-registry/dynamic-component.tsx +0 -58
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { useComponentRegistry } from './component-registry.js';
3
+
4
+ /**
5
+ * @description
6
+ * This component is used to delegate the rendering of a component to the component registry.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * <Delegate component="money.display.default" value={100} />
11
+ * ```
12
+ *
13
+ * @returns
14
+ */
15
+ export function DisplayComponent(
16
+ props: Readonly<{
17
+ id: string;
18
+ value: any;
19
+ }>,
20
+ ): React.ReactNode {
21
+ const { getDisplayComponent } = useComponentRegistry();
22
+ const Component = getDisplayComponent(props.id.toString());
23
+ if (!Component) {
24
+ throw new Error(`Component with id ${props.id.toString()} not found`);
25
+ }
26
+ const { value, ...rest } = props;
27
+ return <Component value={value} {...rest} />;
28
+ }
@@ -22,20 +22,6 @@ export function getDisplayComponent(id: string): DataDisplayComponent | undefine
22
22
  return globalRegistry.get('displayComponents').get(id);
23
23
  }
24
24
 
25
- /**
26
- * @description
27
- * Gets a display component using the targeting properties.
28
- * Uses the same key pattern as registration: pageId_blockId_fieldName
29
- */
30
- export function getTargetedDisplayComponent(
31
- pageId: string,
32
- blockId: string,
33
- field: string,
34
- ): DataDisplayComponent | undefined {
35
- const key = generateDisplayComponentKey(pageId, blockId, field);
36
- return globalRegistry.get('displayComponents').get(key);
37
- }
38
-
39
25
  /**
40
26
  * @description
41
27
  * Generates a display component key based on the targeting properties.
@@ -1,43 +1,45 @@
1
1
  import { CombinationModeInput } from '@/vdb/components/data-input/combination-mode-input.js';
2
- import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
3
- import { FacetValueInput } from '@/vdb/components/data-input/facet-value-input.js';
4
- import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
5
- import { ProductMultiInput } from '@/vdb/components/data-input/product-multi-selector.js';
6
- import { Checkbox } from '@/vdb/components/ui/checkbox.js';
7
- import { Input } from '@/vdb/components/ui/input.js';
8
- import { DataInputComponent } from '../component-registry/component-registry.js';
2
+ import { DefaultRelationInput } from '@/vdb/components/data-input/default-relation-input.js';
3
+ import {
4
+ CustomerGroupInput,
5
+ FacetValueInput,
6
+ MoneyInput,
7
+ ProductMultiInput,
8
+ RichTextInput,
9
+ SelectWithOptions,
10
+ } from '@/vdb/components/data-input/index.js';
11
+ import { PasswordInput } from '@/vdb/components/data-input/password-input.js';
12
+ import { TextareaInput } from '@/vdb/components/data-input/textarea-input.js';
13
+ import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
9
14
  import { globalRegistry } from '../registry/global-registry.js';
10
15
 
11
- globalRegistry.register('inputComponents', new Map<string, DataInputComponent>());
12
-
13
- // Create component functions for built-in components
14
- const TextInput: DataInputComponent = props => (
15
- <Input {...props} onChange={e => props.onChange(e.target.value)} />
16
- );
17
- const NumberInput: DataInputComponent = props => (
18
- <Input {...props} onChange={e => props.onChange(e.target.valueAsNumber)} type="number" />
19
- );
20
- const CheckboxInput: DataInputComponent = props => (
21
- <Checkbox
22
- {...props}
23
- checked={props.value === 'true' || props.value === true}
24
- onCheckedChange={value => props.onChange(value)}
25
- />
26
- );
16
+ globalRegistry.register('inputComponents', new Map<string, DashboardFormComponent>());
27
17
 
28
18
  // Register built-in input components
29
19
  const inputComponents = globalRegistry.get('inputComponents');
30
- inputComponents.set('vendure:moneyInput', MoneyInput);
31
- inputComponents.set('vendure:textInput', TextInput);
32
- inputComponents.set('vendure:numberInput', NumberInput);
33
- inputComponents.set('vendure:dateTimeInput', DateTimeInput);
34
- inputComponents.set('vendure:checkboxInput', CheckboxInput);
35
- inputComponents.set('vendure:facetValueInput', FacetValueInput);
36
- inputComponents.set('vendure:combinationModeInput', CombinationModeInput);
37
- inputComponents.set('vendure:productMultiInput', ProductMultiInput);
20
+ inputComponents.set('facet-value-input', FacetValueInput);
21
+ inputComponents.set('combination-mode-input', CombinationModeInput);
22
+ inputComponents.set('product-multi-input', ProductMultiInput);
23
+ inputComponents.set('currency-form-input', MoneyInput);
24
+ inputComponents.set('customer-group-form-input', CustomerGroupInput);
25
+ inputComponents.set('facet-value-form-input', FacetValueInput);
26
+ inputComponents.set('json-editor-form-input', TextareaInput);
27
+ inputComponents.set('textarea-form-input', TextareaInput);
28
+ inputComponents.set('html-editor-form-input', RichTextInput);
29
+ inputComponents.set('rich-text-form-input', RichTextInput);
30
+ inputComponents.set('password-form-input', PasswordInput);
31
+ inputComponents.set('product-selector-form-input', DefaultRelationInput);
32
+ inputComponents.set('relation-form-input', DefaultRelationInput);
33
+ inputComponents.set('select-form-input', SelectWithOptions);
34
+ inputComponents.set('product-multi-form-input', ProductMultiInput);
35
+ inputComponents.set('combination-mode-form-input', CombinationModeInput);
38
36
 
39
- export function getInputComponent(id: string): DataInputComponent | undefined {
40
- return globalRegistry.get('inputComponents').get(id);
37
+ export function getInputComponent(id: string | undefined): DashboardFormComponent | undefined {
38
+ if (!id) {
39
+ return undefined;
40
+ }
41
+ const inputComponent = globalRegistry.get('inputComponents').get(id);
42
+ return inputComponent;
41
43
  }
42
44
 
43
45
  /**
@@ -58,7 +60,7 @@ export function addInputComponent({
58
60
  pageId: string;
59
61
  blockId: string;
60
62
  field: string;
61
- component: DataInputComponent;
63
+ component: DashboardFormComponent;
62
64
  }) {
63
65
  const inputComponents = globalRegistry.get('inputComponents');
64
66
 
@@ -71,3 +73,19 @@ export function addInputComponent({
71
73
  }
72
74
  inputComponents.set(key, component);
73
75
  }
76
+
77
+ export function addCustomFieldInputComponent({
78
+ id,
79
+ component,
80
+ }: {
81
+ id: string;
82
+ component: DashboardFormComponent;
83
+ }) {
84
+ const inputComponents = globalRegistry.get('inputComponents');
85
+
86
+ if (inputComponents.has(id)) {
87
+ // eslint-disable-next-line no-console
88
+ console.warn(`Input component with key "${id}" is already registered and will be overwritten.`);
89
+ }
90
+ inputComponents.set(id, component);
91
+ }
@@ -4,28 +4,6 @@ import { addBulkAction, addListQueryDocument } from '../../data-table/data-table
4
4
  import { addDisplayComponent } from '../display-component-extensions.js';
5
5
  import { DashboardDataTableExtensionDefinition } from '../types/index.js';
6
6
 
7
- /**
8
- * @description
9
- * Generates a data table display component key based on the pageId and column name.
10
- * Uses the pattern: pageId_columnName
11
- */
12
- export function generateDataTableDisplayComponentKey(pageId: string, column: string): string {
13
- return `${pageId}_${column}`;
14
- }
15
-
16
- /**
17
- * @description
18
- * Adds a display component for a specific column in a data table.
19
- */
20
- export function addDataTableDisplayComponent(
21
- pageId: string,
22
- column: string,
23
- component: React.ComponentType<{ value: any; [key: string]: any }>,
24
- ) {
25
- const key = generateDataTableDisplayComponentKey(pageId, column);
26
- addDisplayComponent({ pageId, blockId: 'list-table', field: column, component });
27
- }
28
-
29
7
  export function registerDataTableExtensions(dataTables?: DashboardDataTableExtensionDefinition[]) {
30
8
  if (dataTables) {
31
9
  for (const dataTable of dataTables) {
@@ -48,11 +26,10 @@ export function registerDataTableExtensions(dataTables?: DashboardDataTableExten
48
26
  }
49
27
  if (dataTable.displayComponents?.length) {
50
28
  for (const displayComponent of dataTable.displayComponents) {
51
- addDataTableDisplayComponent(
52
- dataTable.pageId,
53
- displayComponent.column,
54
- displayComponent.component,
55
- );
29
+ const blockId = dataTable.blockId ?? 'list-table';
30
+ const { pageId } = dataTable;
31
+ const { column, component } = displayComponent;
32
+ addDisplayComponent({ pageId, blockId, field: column, component });
56
33
  }
57
34
  }
58
35
  }
@@ -1,4 +1,5 @@
1
- import { addCustomFormComponent } from '../../form-engine/custom-form-component-extensions.js';
1
+ import { addCustomFieldInputComponent } from '@/vdb/framework/extension-api/input-component-extensions.js';
2
+
2
3
  import { DashboardCustomFormComponents } from '../types/form-components.js';
3
4
 
4
5
  export function registerFormComponentExtensions(customFormComponents?: DashboardCustomFormComponents) {
@@ -6,7 +7,7 @@ export function registerFormComponentExtensions(customFormComponents?: Dashboard
6
7
  // Handle custom field components
7
8
  if (customFormComponents.customFields) {
8
9
  for (const component of customFormComponents.customFields) {
9
- addCustomFormComponent(component);
10
+ addCustomFieldInputComponent(component);
10
11
  }
11
12
  }
12
13
  }
@@ -1,7 +1,4 @@
1
- import {
2
- DataDisplayComponent,
3
- DataInputComponent,
4
- } from '@/vdb/framework/component-registry/component-registry.js';
1
+ import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
5
2
  import { DocumentNode } from 'graphql';
6
3
 
7
4
  /**
@@ -29,35 +26,7 @@ export interface DashboardDetailFormInputComponent {
29
26
  * The React component that will be rendered as the input.
30
27
  * It should accept `value`, `onChange`, and other standard input props.
31
28
  */
32
- component: DataInputComponent;
33
- }
34
-
35
- /**
36
- * @description
37
- * Allows you to define custom display components for specific fields in detail forms.
38
- * The pageId is already defined in the detail form extension, so only the blockId and field are needed.
39
- *
40
- * @docsCategory extensions
41
- * @docsPage DetailForms
42
- * @since 3.4.0
43
- */
44
- export interface DashboardDetailFormDisplayComponent {
45
- /**
46
- * @description
47
- * The ID of the block where this display component should be used.
48
- */
49
- blockId: string;
50
- /**
51
- * @description
52
- * The name of the field where this display component should be used.
53
- */
54
- field: string;
55
- /**
56
- * @description
57
- * The React component that will be rendered as the display.
58
- * It should accept `value` and other standard display props.
59
- */
60
- component: DataDisplayComponent;
29
+ component: DashboardFormComponent;
61
30
  }
62
31
 
63
32
  /**
@@ -86,9 +55,4 @@ export interface DashboardDetailFormExtensionDefinition {
86
55
  * Custom input components for specific fields in the detail form.
87
56
  */
88
57
  inputs?: DashboardDetailFormInputComponent[];
89
- /**
90
- * @description
91
- * Custom display components for specific fields in the detail form.
92
- */
93
- displays?: DashboardDetailFormDisplayComponent[];
94
58
  }
@@ -1,6 +1,4 @@
1
- import type React from 'react';
2
-
3
- import { CustomFormComponentInputProps } from '../../form-engine/custom-form-component.js';
1
+ import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
4
2
 
5
3
  /**
6
4
  * @description
@@ -21,7 +19,7 @@ export interface DashboardCustomFormComponent {
21
19
  * @description
22
20
  * The React component that will be rendered as the custom form input.
23
21
  */
24
- component: React.FunctionComponent<CustomFormComponentInputProps>;
22
+ component: DashboardFormComponent;
25
23
  }
26
24
 
27
25
  /**
@@ -1,32 +1,9 @@
1
1
  import { DocumentNode } from 'graphql';
2
2
 
3
- import { DashboardCustomFormComponent } from '../extension-api/extension-api-types.js';
4
3
  import { globalRegistry } from '../registry/global-registry.js';
5
4
 
6
- import { CustomFormComponentInputProps } from './custom-form-component.js';
7
-
8
- globalRegistry.register(
9
- 'customFormComponents',
10
- new Map<string, React.FunctionComponent<CustomFormComponentInputProps>>(),
11
- );
12
-
13
5
  globalRegistry.register('detailQueryDocumentRegistry', new Map<string, DocumentNode[]>());
14
6
 
15
- export function getCustomFormComponent(
16
- id: string,
17
- ): React.FunctionComponent<CustomFormComponentInputProps> | undefined {
18
- return globalRegistry.get('customFormComponents').get(id);
19
- }
20
-
21
- export function addCustomFormComponent({ id, component }: DashboardCustomFormComponent) {
22
- const customFormComponents = globalRegistry.get('customFormComponents');
23
- if (customFormComponents.has(id)) {
24
- // eslint-disable-next-line no-console
25
- console.warn(`Custom form component with id "${id}" is already registered and will be overwritten.`);
26
- }
27
- customFormComponents.set(id, component);
28
- }
29
-
30
7
  export function getDetailQueryDocuments(pageId: string): DocumentNode[] {
31
8
  return globalRegistry.get('detailQueryDocumentRegistry').get(pageId) || [];
32
9
  }
@@ -1,33 +1,16 @@
1
- import { CustomFieldConfig } from '@vendure/common/lib/generated-types';
2
- import {
3
- ControllerFieldState,
4
- ControllerRenderProps,
5
- FieldPath,
6
- FieldValues,
7
- UseFormStateReturn,
8
- } from 'react-hook-form';
9
- import { getCustomFormComponent } from './custom-form-component-extensions.js';
1
+ import { getInputComponent } from '@/vdb/framework/extension-api/input-component-extensions.js';
10
2
 
11
- export interface CustomFormComponentProps {
12
- fieldProps: CustomFormComponentInputProps;
13
- fieldDef: Pick<CustomFieldConfig, 'ui' | 'type' | 'name'>;
14
- }
15
-
16
- export interface CustomFormComponentInputProps<
17
- TFieldValues extends FieldValues = FieldValues,
18
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
19
- > {
20
- field: ControllerRenderProps<TFieldValues, TName>;
21
- fieldState: ControllerFieldState;
22
- formState: UseFormStateReturn<TFieldValues>;
23
- }
3
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
24
4
 
25
- export function CustomFormComponent({ fieldDef, fieldProps }: CustomFormComponentProps) {
26
- const Component = getCustomFormComponent(fieldDef.ui?.component);
5
+ export function CustomFormComponent(props: DashboardFormComponentProps) {
6
+ if (!props.fieldDef) {
7
+ return null;
8
+ }
9
+ const Component = getInputComponent(props.fieldDef.ui?.component);
27
10
 
28
11
  if (!Component) {
29
12
  return null;
30
13
  }
31
14
 
32
- return <Component {...fieldProps} />;
15
+ return <Component {...props} />;
33
16
  }
@@ -0,0 +1,35 @@
1
+ import { BooleanInput } from '@/vdb/components/data-input/boolean-input.js';
2
+ import { DefaultRelationInput } from '@/vdb/components/data-input/default-relation-input.js';
3
+ import { DateTimeInput, SelectWithOptions } from '@/vdb/components/data-input/index.js';
4
+ import { NumberInput } from '@/vdb/components/data-input/number-input.js';
5
+ import { TextInput } from '@/vdb/components/data-input/text-input.js';
6
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
7
+ import { isStringFieldWithOptions } from '@/vdb/framework/form-engine/utils.js';
8
+
9
+ /**
10
+ * Consolidated input component for rendering form inputs based on field type
11
+ * This replaces the duplicate implementations in custom fields and config args
12
+ */
13
+ export function DefaultInputForType({ fieldDef, ...fieldProps }: Readonly<DashboardFormComponentProps>) {
14
+ const type = fieldDef?.type;
15
+ switch (type) {
16
+ case 'int':
17
+ case 'float':
18
+ return <NumberInput {...fieldProps} fieldDef={fieldDef} />;
19
+ case 'boolean':
20
+ return <BooleanInput {...fieldProps} fieldDef={fieldDef} />;
21
+ case 'datetime':
22
+ return <DateTimeInput {...fieldProps} fieldDef={fieldDef} />;
23
+ case 'relation':
24
+ return <DefaultRelationInput {...fieldProps} fieldDef={fieldDef} />;
25
+ case 'string': {
26
+ if (fieldDef && isStringFieldWithOptions(fieldDef)) {
27
+ return <SelectWithOptions {...fieldProps} fieldDef={fieldDef} />;
28
+ } else {
29
+ return <TextInput {...fieldProps} fieldDef={fieldDef} />;
30
+ }
31
+ }
32
+ default:
33
+ return <TextInput {...fieldProps} fieldDef={fieldDef} />;
34
+ }
35
+ }
@@ -0,0 +1,192 @@
1
+ import { JSX, useMemo } from 'react';
2
+ import { ControllerRenderProps } from 'react-hook-form';
3
+
4
+ import { CustomFieldListInput } from '@/vdb/components/data-input/custom-field-list-input.js';
5
+ import { StructFormInput } from '@/vdb/components/data-input/struct-form-input.js';
6
+ import { ConfigurableOperationListInput } from '../../components/data-input/configurable-operation-list-input.js';
7
+
8
+ import { getInputComponent } from '@/vdb/framework/extension-api/input-component-extensions.js';
9
+ import {
10
+ ConfigurableFieldDef,
11
+ DashboardFormComponent,
12
+ } from '@/vdb/framework/form-engine/form-engine-types.js';
13
+ import { isCustomFieldConfig } from '@/vdb/framework/form-engine/utils.js';
14
+ import { DefaultInputForType } from './default-input-for-type.js';
15
+ import { transformValue, ValueMode } from './value-transformers.js';
16
+
17
+ export interface FormControlAdapterProps {
18
+ fieldDef: ConfigurableFieldDef;
19
+ field: ControllerRenderProps<any, any>;
20
+ valueMode: ValueMode;
21
+ }
22
+
23
+ /**
24
+ * Gets the default value for list inputs based on field type
25
+ */
26
+ function getDefaultValueForType(fieldType: string): any {
27
+ switch (fieldType) {
28
+ case 'string':
29
+ case 'localeString':
30
+ case 'localeText':
31
+ return '';
32
+ case 'int':
33
+ case 'float':
34
+ return 0;
35
+ case 'boolean':
36
+ return false;
37
+ default:
38
+ return '';
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Validates if a custom component is correctly configured for list fields
44
+ */
45
+ function validateCustomComponent(
46
+ CustomComponent: any,
47
+ componentId: string,
48
+ fieldName: string,
49
+ isList: boolean,
50
+ ): void {
51
+ if (!CustomComponent.metadata?.isListInput || CustomComponent.metadata.isListInput === 'dynamic') {
52
+ return;
53
+ }
54
+
55
+ const isConfiguredForList = CustomComponent.metadata.isListInput === true;
56
+ if (isConfiguredForList !== isList) {
57
+ // eslint-disable-next-line no-console
58
+ console.warn([
59
+ `Custom component ${componentId} is not correctly configured for the ${fieldName} field:`,
60
+ `The component ${isConfiguredForList ? 'is' : 'is not'} configured as a list input, but the field ${isList ? 'is' : 'is not'} a list field.`,
61
+ ]);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Determines if a custom component can be used for the given field configuration
67
+ */
68
+ function canUseCustomComponent(
69
+ CustomComponent: DashboardFormComponent | undefined,
70
+ isList: boolean,
71
+ ): CustomComponent is DashboardFormComponent {
72
+ if (!CustomComponent) return false;
73
+
74
+ const listInputMode = CustomComponent.metadata?.isListInput;
75
+
76
+ // Dynamic components can handle both list and non-list
77
+ if (listInputMode === 'dynamic') return true;
78
+
79
+ // Exact match: both are list or both are non-list
80
+ return (isList && listInputMode === true) || (!isList && listInputMode !== true);
81
+ }
82
+
83
+ /**
84
+ * Renders struct field inputs
85
+ */
86
+ function renderStructField(
87
+ fieldDef: ConfigurableFieldDef,
88
+ field: ControllerRenderProps<any, any>,
89
+ fieldWithTransform: ControllerRenderProps<any, any>,
90
+ isList: boolean,
91
+ isReadonly: boolean,
92
+ ): JSX.Element {
93
+ if (isList) {
94
+ return (
95
+ <CustomFieldListInput
96
+ {...field}
97
+ disabled={isReadonly}
98
+ renderInput={(index, inputField) => (
99
+ <StructFormInput {...fieldWithTransform} fieldDef={fieldDef} />
100
+ )}
101
+ defaultValue={{}}
102
+ />
103
+ );
104
+ }
105
+ return <StructFormInput {...fieldWithTransform} fieldDef={fieldDef} />;
106
+ }
107
+
108
+ /**
109
+ * Renders list field inputs
110
+ */
111
+ function renderListField(
112
+ fieldDef: ConfigurableFieldDef,
113
+ field: ControllerRenderProps<any, any>,
114
+ fieldWithTransform: ControllerRenderProps<any, any>,
115
+ valueMode: ValueMode,
116
+ isReadonly: boolean,
117
+ ): JSX.Element {
118
+ if (valueMode === 'json-string') {
119
+ return <ConfigurableOperationListInput {...fieldWithTransform} fieldDef={fieldDef} />;
120
+ }
121
+
122
+ if (fieldDef.type === 'relation') {
123
+ return <DefaultInputForType {...fieldWithTransform} fieldDef={fieldDef} />;
124
+ }
125
+
126
+ if (fieldDef.type === 'string') {
127
+ return <DefaultInputForType {...fieldWithTransform} fieldDef={fieldDef} />;
128
+ }
129
+
130
+ return (
131
+ <CustomFieldListInput
132
+ {...field}
133
+ disabled={isReadonly}
134
+ renderInput={(index, inputField) => <DefaultInputForType {...inputField} fieldDef={fieldDef} />}
135
+ defaultValue={getDefaultValueForType(fieldDef.type)}
136
+ />
137
+ );
138
+ }
139
+
140
+ /**
141
+ * This is a wrapper component around the final DashboardFormComponent instances.
142
+ *
143
+ * It is responsible for ensuring the correct props get passed to the final component,
144
+ * and for handling differences between form control use between:
145
+ *
146
+ * - Auto-generated forms
147
+ * - Custom field forms
148
+ * - Configurable operation forms
149
+ */
150
+ export function FormControlAdapter({ fieldDef, field, valueMode }: Readonly<FormControlAdapterProps>) {
151
+ const isList = fieldDef.list ?? false;
152
+ const isReadonly = isCustomFieldConfig(fieldDef) ? fieldDef.readonly === true : false;
153
+ const componentId = fieldDef.ui?.component as string | undefined;
154
+
155
+ const fieldWithTransform = useMemo(() => {
156
+ const fieldOnChange = field.onChange.bind(field);
157
+ const transformedField: FormControlAdapterProps['field'] = {
158
+ ...field,
159
+ value: transformValue(field.value, fieldDef, valueMode, 'parse'),
160
+ onChange: (newValue: any) => {
161
+ const serializedValue = transformValue(newValue, fieldDef, valueMode, 'serialize');
162
+ fieldOnChange(serializedValue);
163
+ },
164
+ };
165
+ return transformedField;
166
+ }, [field.name, field.value, field.onChange, fieldDef, valueMode]);
167
+
168
+ const CustomComponent = getInputComponent(componentId);
169
+
170
+ // Try to use custom component if available and compatible
171
+ if (canUseCustomComponent(CustomComponent, isList)) {
172
+ return <CustomComponent {...fieldWithTransform} fieldDef={fieldDef} />;
173
+ }
174
+
175
+ // Validate custom component configuration for debugging
176
+ if (CustomComponent) {
177
+ validateCustomComponent(CustomComponent, componentId!, fieldDef.name, isList);
178
+ }
179
+
180
+ // Handle struct fields (custom fields mode only)
181
+ if (fieldDef.type === 'struct' && valueMode === 'native') {
182
+ return renderStructField(fieldDef, field, fieldWithTransform, isList, isReadonly);
183
+ }
184
+
185
+ // Handle list fields
186
+ if (isList) {
187
+ return renderListField(fieldDef, field, fieldWithTransform, valueMode, isReadonly);
188
+ }
189
+
190
+ // Default case: non-list, non-struct fields
191
+ return <DefaultInputForType {...fieldWithTransform} fieldDef={fieldDef} />;
192
+ }