@vendure/dashboard 3.4.1-master-202508050244 → 3.4.1-master-202508070243
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/package.json +4 -4
- package/src/lib/components/data-input/money-input.tsx +1 -0
- package/src/lib/components/data-input/rich-text-input.tsx +21 -7
- package/src/lib/components/shared/asset/asset-gallery.tsx +9 -2
- package/src/lib/components/shared/asset/asset-picker-dialog.tsx +1 -0
- package/src/lib/components/shared/configurable-operation-arg-input.tsx +13 -372
- package/src/lib/components/shared/custom-fields-form.tsx +19 -143
- package/src/lib/components/shared/direct-form-component-map.tsx +393 -0
- package/src/lib/components/shared/universal-field-definition.ts +118 -0
- package/src/lib/components/shared/universal-form-input.tsx +175 -0
- package/src/lib/components/shared/universal-input-components.tsx +291 -0
- package/src/lib/components/shared/value-transformers.ts +143 -0
- package/src/lib/framework/form-engine/form-schema-tools.spec.ts +138 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +4 -4
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
|
|
2
|
+
import { CustomFieldConfig } from '@vendure/common/lib/generated-types';
|
|
3
|
+
import { ConfigArgType, CustomFieldType, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Universal field definition that can represent both custom fields and configurable operation args
|
|
7
|
+
*/
|
|
8
|
+
export interface UniversalFieldDefinition {
|
|
9
|
+
name: string;
|
|
10
|
+
type: CustomFieldType | 'ID'; // Extends CustomFieldType with ID for config args
|
|
11
|
+
list?: boolean;
|
|
12
|
+
readonly?: boolean;
|
|
13
|
+
ui?: {
|
|
14
|
+
component?: DefaultFormComponentId | string;
|
|
15
|
+
options?: Array<{ value: string; label: string | Array<{ languageCode: string; value: string }> }>;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
prefix?: string;
|
|
20
|
+
suffix?: string;
|
|
21
|
+
tab?: string;
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
spellcheck?: boolean;
|
|
24
|
+
selectionMode?: string;
|
|
25
|
+
};
|
|
26
|
+
entity?: string; // for relations
|
|
27
|
+
label?: string | Array<{ languageCode: string; value: string }>;
|
|
28
|
+
description?: string | Array<{ languageCode: string; value: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert a custom field config to universal field definition
|
|
33
|
+
*/
|
|
34
|
+
export function customFieldToUniversal(fieldDef: CustomFieldConfig): UniversalFieldDefinition {
|
|
35
|
+
const hasOptions = (fieldDef as any).options;
|
|
36
|
+
const hasUi = fieldDef.ui;
|
|
37
|
+
const hasNumericConfig =
|
|
38
|
+
(fieldDef as any).min !== undefined ||
|
|
39
|
+
(fieldDef as any).max !== undefined ||
|
|
40
|
+
(fieldDef as any).step !== undefined;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: fieldDef.name,
|
|
44
|
+
type: fieldDef.type as any,
|
|
45
|
+
list: fieldDef.list ?? false,
|
|
46
|
+
readonly: fieldDef.readonly ?? false,
|
|
47
|
+
ui:
|
|
48
|
+
hasUi || hasOptions || hasNumericConfig
|
|
49
|
+
? {
|
|
50
|
+
component: fieldDef.ui?.component,
|
|
51
|
+
options: (fieldDef as any).options,
|
|
52
|
+
...((fieldDef as any).min != null && {
|
|
53
|
+
min: (fieldDef as any).min,
|
|
54
|
+
}),
|
|
55
|
+
...((fieldDef as any).max != null && {
|
|
56
|
+
max: (fieldDef as any).max,
|
|
57
|
+
}),
|
|
58
|
+
...((fieldDef as any).step != null && {
|
|
59
|
+
step: (fieldDef as any).step,
|
|
60
|
+
}),
|
|
61
|
+
tab: fieldDef.ui?.tab,
|
|
62
|
+
fullWidth: fieldDef.ui?.fullWidth,
|
|
63
|
+
}
|
|
64
|
+
: undefined,
|
|
65
|
+
entity: (fieldDef as any).entity,
|
|
66
|
+
label: fieldDef.label,
|
|
67
|
+
description: fieldDef.description,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert a configurable operation arg definition to universal field definition
|
|
73
|
+
*/
|
|
74
|
+
export function configArgToUniversal(
|
|
75
|
+
definition: ConfigurableOperationDefFragment['args'][number],
|
|
76
|
+
): UniversalFieldDefinition {
|
|
77
|
+
const ui = definition.ui;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
name: definition.name,
|
|
81
|
+
type: mapConfigArgType(definition.type as ConfigArgType),
|
|
82
|
+
list: definition.list ?? false,
|
|
83
|
+
readonly: false,
|
|
84
|
+
ui: ui
|
|
85
|
+
? {
|
|
86
|
+
component: ui.component,
|
|
87
|
+
options: ui.options,
|
|
88
|
+
min: ui.min ?? undefined,
|
|
89
|
+
max: ui.max ?? undefined,
|
|
90
|
+
step: ui.step ?? undefined,
|
|
91
|
+
prefix: ui.prefix,
|
|
92
|
+
suffix: ui.suffix,
|
|
93
|
+
spellcheck: ui.spellcheck,
|
|
94
|
+
selectionMode: ui.selectionMode,
|
|
95
|
+
}
|
|
96
|
+
: undefined,
|
|
97
|
+
entity: getEntityFromUiComponent(ui?.component),
|
|
98
|
+
label: definition.label,
|
|
99
|
+
description: definition.description,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function mapConfigArgType(configArgType: ConfigArgType): UniversalFieldDefinition['type'] {
|
|
104
|
+
// All ConfigArgType values are compatible with our extended type
|
|
105
|
+
return configArgType as UniversalFieldDefinition['type'];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getEntityFromUiComponent(component?: string): string | undefined {
|
|
109
|
+
switch (component) {
|
|
110
|
+
case 'product-selector-form-input':
|
|
111
|
+
case 'product-multi-form-input':
|
|
112
|
+
return 'Product';
|
|
113
|
+
case 'customer-group-form-input':
|
|
114
|
+
return 'CustomerGroup';
|
|
115
|
+
default:
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { DefaultFormComponentId } from '@vendure/common/lib/shared-types';
|
|
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 {
|
|
7
|
+
CustomFormComponent,
|
|
8
|
+
CustomFormComponentInputProps,
|
|
9
|
+
} from '@/vdb/framework/form-engine/custom-form-component.js';
|
|
10
|
+
|
|
11
|
+
import { ConfigurableOperationListInput } from '../data-input/configurable-operation-list-input.js';
|
|
12
|
+
import { FacetValueInput } from '../data-input/facet-value-input.js';
|
|
13
|
+
import { getDirectFormComponent } from './direct-form-component-map.js';
|
|
14
|
+
import { UniversalFieldDefinition } from './universal-field-definition.js';
|
|
15
|
+
import { UniversalInputComponent } from './universal-input-components.js';
|
|
16
|
+
import { ValueMode } from './value-transformers.js';
|
|
17
|
+
|
|
18
|
+
export interface UniversalFormInputProps {
|
|
19
|
+
fieldDef: UniversalFieldDefinition;
|
|
20
|
+
field: ControllerRenderProps<any, any>;
|
|
21
|
+
valueMode: ValueMode;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
// Additional props for config args mode
|
|
24
|
+
position?: number;
|
|
25
|
+
// Additional props for custom fields mode
|
|
26
|
+
control?: any;
|
|
27
|
+
getTranslation?: (
|
|
28
|
+
input: Array<{ languageCode: string; value: string }> | null | undefined,
|
|
29
|
+
) => string | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Universal form input component that handles both custom fields and configurable operation args
|
|
34
|
+
* Maintains full backward compatibility with existing APIs while eliminating duplication
|
|
35
|
+
*/
|
|
36
|
+
export function UniversalFormInput({
|
|
37
|
+
fieldDef,
|
|
38
|
+
field,
|
|
39
|
+
valueMode,
|
|
40
|
+
disabled = false,
|
|
41
|
+
position,
|
|
42
|
+
control,
|
|
43
|
+
getTranslation,
|
|
44
|
+
}: Readonly<UniversalFormInputProps>) {
|
|
45
|
+
const uiComponent = fieldDef.ui?.component;
|
|
46
|
+
const isList = fieldDef.list ?? false;
|
|
47
|
+
const isReadonly = disabled || fieldDef.readonly;
|
|
48
|
+
|
|
49
|
+
// Handle special case: facet-value-form-input (only in config args)
|
|
50
|
+
if (uiComponent === 'facet-value-form-input' && valueMode === 'json-string') {
|
|
51
|
+
return <FacetValueInput value={field.value} onChange={field.onChange} readOnly={isReadonly} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle custom form components (custom fields mode)
|
|
55
|
+
if (uiComponent && valueMode === 'native') {
|
|
56
|
+
const fieldProps: CustomFormComponentInputProps = {
|
|
57
|
+
field: {
|
|
58
|
+
...field,
|
|
59
|
+
disabled: isReadonly,
|
|
60
|
+
},
|
|
61
|
+
fieldState: {} as any, // This would be passed from the parent FormField
|
|
62
|
+
formState: {} as any, // This would be passed from the parent FormField
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return <CustomFormComponent fieldDef={fieldDef as any} fieldProps={fieldProps} />;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle direct component mapping (config args mode)
|
|
69
|
+
if (uiComponent && valueMode === 'json-string') {
|
|
70
|
+
const DirectComponent = getDirectFormComponent(uiComponent as DefaultFormComponentId);
|
|
71
|
+
if (DirectComponent) {
|
|
72
|
+
return (
|
|
73
|
+
<DirectComponent
|
|
74
|
+
fieldDef={fieldDef}
|
|
75
|
+
field={field}
|
|
76
|
+
valueMode={valueMode}
|
|
77
|
+
disabled={isReadonly}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle struct fields (custom fields mode only)
|
|
84
|
+
if (fieldDef.type === 'struct' && valueMode === 'native') {
|
|
85
|
+
if (isList) {
|
|
86
|
+
return (
|
|
87
|
+
<CustomFieldListInput
|
|
88
|
+
field={field}
|
|
89
|
+
disabled={isReadonly}
|
|
90
|
+
renderInput={(index, inputField) => (
|
|
91
|
+
<StructFormInput
|
|
92
|
+
field={inputField}
|
|
93
|
+
fieldDef={fieldDef as any}
|
|
94
|
+
control={control}
|
|
95
|
+
getTranslation={getTranslation}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
defaultValue={{}}
|
|
99
|
+
isFullWidth={true}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<StructFormInput
|
|
106
|
+
field={field}
|
|
107
|
+
fieldDef={fieldDef as any}
|
|
108
|
+
control={control}
|
|
109
|
+
getTranslation={getTranslation}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle list fields
|
|
115
|
+
if (isList) {
|
|
116
|
+
if (valueMode === 'json-string') {
|
|
117
|
+
// Use ConfigurableOperationListInput for config args
|
|
118
|
+
return (
|
|
119
|
+
<ConfigurableOperationListInput
|
|
120
|
+
definition={fieldDef as any}
|
|
121
|
+
value={field.value}
|
|
122
|
+
onChange={field.onChange}
|
|
123
|
+
readOnly={isReadonly}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
// Use CustomFieldListInput for custom fields
|
|
128
|
+
const getDefaultValue = () => {
|
|
129
|
+
switch (fieldDef.type) {
|
|
130
|
+
case 'string':
|
|
131
|
+
case 'localeString':
|
|
132
|
+
case 'localeText':
|
|
133
|
+
return '';
|
|
134
|
+
case 'int':
|
|
135
|
+
case 'float':
|
|
136
|
+
return 0;
|
|
137
|
+
case 'boolean':
|
|
138
|
+
return false;
|
|
139
|
+
case 'datetime':
|
|
140
|
+
return '';
|
|
141
|
+
case 'relation':
|
|
142
|
+
return '';
|
|
143
|
+
default:
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<CustomFieldListInput
|
|
150
|
+
field={field}
|
|
151
|
+
disabled={isReadonly}
|
|
152
|
+
renderInput={(index, inputField) => (
|
|
153
|
+
<UniversalInputComponent
|
|
154
|
+
fieldDef={{ ...fieldDef, list: false }}
|
|
155
|
+
field={inputField}
|
|
156
|
+
valueMode={valueMode}
|
|
157
|
+
disabled={isReadonly}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
defaultValue={getDefaultValue()}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fall back to consolidated input component
|
|
167
|
+
return (
|
|
168
|
+
<UniversalInputComponent
|
|
169
|
+
fieldDef={fieldDef}
|
|
170
|
+
field={field}
|
|
171
|
+
valueMode={valueMode}
|
|
172
|
+
disabled={isReadonly}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ControllerRenderProps } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
import { AffixedInput } from '../data-input/affixed-input.js';
|
|
5
|
+
import { DateTimeInput } from '../data-input/datetime-input.js';
|
|
6
|
+
import { DefaultRelationInput } from '../data-input/default-relation-input.js';
|
|
7
|
+
import { SelectWithOptions } from '../data-input/select-with-options.js';
|
|
8
|
+
import { Input } from '../ui/input.js';
|
|
9
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
|
|
10
|
+
import { Switch } from '../ui/switch.js';
|
|
11
|
+
import { Textarea } from '../ui/textarea.js';
|
|
12
|
+
import { UniversalFieldDefinition } from './universal-field-definition.js';
|
|
13
|
+
import { ValueMode, transformValue } from './value-transformers.js';
|
|
14
|
+
|
|
15
|
+
export interface UniversalInputComponentProps {
|
|
16
|
+
fieldDef: UniversalFieldDefinition;
|
|
17
|
+
field: ControllerRenderProps<any, any>;
|
|
18
|
+
valueMode: ValueMode;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Component renderer interface for cleaner separation
|
|
23
|
+
interface ComponentRendererProps {
|
|
24
|
+
fieldDef: UniversalFieldDefinition;
|
|
25
|
+
field: ControllerRenderProps<any, any>;
|
|
26
|
+
valueMode: ValueMode;
|
|
27
|
+
isReadonly: boolean;
|
|
28
|
+
transformedValue: any;
|
|
29
|
+
handleChange: (value: any) => void;
|
|
30
|
+
handleNumericChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
31
|
+
handleRegularNumericChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
32
|
+
handleTextareaChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
33
|
+
handleTextChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Renders relation input component
|
|
38
|
+
*/
|
|
39
|
+
function renderRelationInput({ fieldDef, field, transformedValue, handleChange, isReadonly }: ComponentRendererProps) {
|
|
40
|
+
if (fieldDef.type !== 'relation' || !fieldDef.entity) return null;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<DefaultRelationInput
|
|
44
|
+
fieldDef={{
|
|
45
|
+
entity: fieldDef.entity,
|
|
46
|
+
list: fieldDef.list,
|
|
47
|
+
} as any}
|
|
48
|
+
field={{
|
|
49
|
+
...field,
|
|
50
|
+
value: transformedValue,
|
|
51
|
+
onChange: handleChange,
|
|
52
|
+
}}
|
|
53
|
+
disabled={isReadonly}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Renders string field with options as select dropdown
|
|
60
|
+
*/
|
|
61
|
+
function renderSelectInput({ fieldDef, valueMode, transformedValue, handleChange, isReadonly, field }: ComponentRendererProps) {
|
|
62
|
+
if (fieldDef.type !== 'string' || !fieldDef.ui?.options) return null;
|
|
63
|
+
|
|
64
|
+
if (valueMode === 'json-string') {
|
|
65
|
+
return (
|
|
66
|
+
<Select value={transformedValue || ''} onValueChange={handleChange} disabled={isReadonly}>
|
|
67
|
+
<SelectTrigger className="bg-background mb-0">
|
|
68
|
+
<SelectValue placeholder="Select an option..." />
|
|
69
|
+
</SelectTrigger>
|
|
70
|
+
<SelectContent>
|
|
71
|
+
{fieldDef.ui.options.map((option) => (
|
|
72
|
+
<SelectItem key={option.value} value={option.value}>
|
|
73
|
+
{typeof option.label === 'string'
|
|
74
|
+
? option.label
|
|
75
|
+
: Array.isArray(option.label)
|
|
76
|
+
? option.label[0]?.value || option.value
|
|
77
|
+
: option.value}
|
|
78
|
+
</SelectItem>
|
|
79
|
+
))}
|
|
80
|
+
</SelectContent>
|
|
81
|
+
</Select>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<SelectWithOptions
|
|
87
|
+
field={{
|
|
88
|
+
...field,
|
|
89
|
+
value: transformedValue,
|
|
90
|
+
onChange: handleChange,
|
|
91
|
+
}}
|
|
92
|
+
options={fieldDef.ui.options as any}
|
|
93
|
+
disabled={isReadonly}
|
|
94
|
+
isListField={fieldDef.list}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Renders numeric input components (int/float)
|
|
101
|
+
*/
|
|
102
|
+
function renderNumericInput({ fieldDef, valueMode, transformedValue, handleNumericChange, handleRegularNumericChange, isReadonly, field }: ComponentRendererProps) {
|
|
103
|
+
if (fieldDef.type !== 'int' && fieldDef.type !== 'float') return null;
|
|
104
|
+
|
|
105
|
+
const isFloat = fieldDef.type === 'float';
|
|
106
|
+
const min = fieldDef.ui?.min;
|
|
107
|
+
const max = fieldDef.ui?.max;
|
|
108
|
+
const step = fieldDef.ui?.step || (isFloat ? 0.01 : 1);
|
|
109
|
+
const prefix = fieldDef.ui?.prefix;
|
|
110
|
+
const suffix = fieldDef.ui?.suffix;
|
|
111
|
+
|
|
112
|
+
const shouldUseAffixedInput = prefix || suffix || valueMode === 'json-string';
|
|
113
|
+
|
|
114
|
+
if (shouldUseAffixedInput) {
|
|
115
|
+
const numericValue = transformedValue !== undefined && transformedValue !== ''
|
|
116
|
+
? (typeof transformedValue === 'number' ? transformedValue : parseFloat(transformedValue) || '')
|
|
117
|
+
: '';
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<AffixedInput
|
|
121
|
+
type="number"
|
|
122
|
+
value={numericValue}
|
|
123
|
+
onChange={handleNumericChange}
|
|
124
|
+
disabled={isReadonly}
|
|
125
|
+
min={min}
|
|
126
|
+
max={max}
|
|
127
|
+
step={step}
|
|
128
|
+
prefix={prefix}
|
|
129
|
+
suffix={suffix}
|
|
130
|
+
className="bg-background"
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Input
|
|
137
|
+
type="number"
|
|
138
|
+
value={transformedValue ?? ''}
|
|
139
|
+
onChange={handleRegularNumericChange}
|
|
140
|
+
onBlur={field.onBlur}
|
|
141
|
+
name={field.name}
|
|
142
|
+
disabled={isReadonly}
|
|
143
|
+
min={min}
|
|
144
|
+
max={max}
|
|
145
|
+
step={step}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Renders boolean input as switch
|
|
152
|
+
*/
|
|
153
|
+
function renderBooleanInput({ fieldDef, valueMode, transformedValue, handleChange, isReadonly }: ComponentRendererProps) {
|
|
154
|
+
if (fieldDef.type !== 'boolean') return null;
|
|
155
|
+
|
|
156
|
+
const boolValue = valueMode === 'json-string'
|
|
157
|
+
? (transformedValue === true || transformedValue === 'true')
|
|
158
|
+
: transformedValue;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Switch
|
|
162
|
+
checked={boolValue}
|
|
163
|
+
onCheckedChange={handleChange}
|
|
164
|
+
disabled={isReadonly}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Renders datetime input
|
|
171
|
+
*/
|
|
172
|
+
function renderDateTimeInput({ fieldDef, transformedValue, handleChange, isReadonly }: ComponentRendererProps) {
|
|
173
|
+
if (fieldDef.type !== 'datetime') return null;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<DateTimeInput
|
|
177
|
+
value={transformedValue}
|
|
178
|
+
onChange={handleChange}
|
|
179
|
+
disabled={isReadonly}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Renders textarea for specific config args
|
|
186
|
+
*/
|
|
187
|
+
function renderTextareaInput({ fieldDef, valueMode, transformedValue, handleTextareaChange, isReadonly }: ComponentRendererProps) {
|
|
188
|
+
if (valueMode !== 'json-string' || fieldDef.ui?.component !== 'textarea-form-input') return null;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<Textarea
|
|
192
|
+
value={transformedValue || ''}
|
|
193
|
+
onChange={handleTextareaChange}
|
|
194
|
+
disabled={isReadonly}
|
|
195
|
+
spellCheck={fieldDef.ui?.spellcheck ?? true}
|
|
196
|
+
placeholder="Enter text..."
|
|
197
|
+
rows={4}
|
|
198
|
+
className="bg-background"
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Renders default text input
|
|
205
|
+
*/
|
|
206
|
+
function renderTextInput({ valueMode, transformedValue, handleTextChange, isReadonly, field }: ComponentRendererProps) {
|
|
207
|
+
return (
|
|
208
|
+
<Input
|
|
209
|
+
type="text"
|
|
210
|
+
value={transformedValue ?? ''}
|
|
211
|
+
onChange={handleTextChange}
|
|
212
|
+
onBlur={field.onBlur}
|
|
213
|
+
name={field.name}
|
|
214
|
+
disabled={isReadonly}
|
|
215
|
+
placeholder={valueMode === 'json-string' ? "Enter value..." : undefined}
|
|
216
|
+
className={valueMode === 'json-string' ? "bg-background" : undefined}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Consolidated input component for rendering form inputs based on field type
|
|
223
|
+
* This replaces the duplicate implementations in custom fields and config args
|
|
224
|
+
*/
|
|
225
|
+
export function UniversalInputComponent({
|
|
226
|
+
fieldDef,
|
|
227
|
+
field,
|
|
228
|
+
valueMode,
|
|
229
|
+
disabled = false,
|
|
230
|
+
}: Readonly<UniversalInputComponentProps>) {
|
|
231
|
+
const isReadonly = disabled || fieldDef.readonly;
|
|
232
|
+
|
|
233
|
+
// Transform the field value for the component
|
|
234
|
+
const transformedValue = React.useMemo(() => {
|
|
235
|
+
return valueMode === 'json-string'
|
|
236
|
+
? transformValue(field.value, fieldDef, valueMode, 'parse')
|
|
237
|
+
: field.value;
|
|
238
|
+
}, [field.value, fieldDef, valueMode]);
|
|
239
|
+
|
|
240
|
+
// Transform onChange handler for the component
|
|
241
|
+
const handleChange = React.useCallback((newValue: any) => {
|
|
242
|
+
const serializedValue = valueMode === 'json-string'
|
|
243
|
+
? transformValue(newValue, fieldDef, valueMode, 'serialize')
|
|
244
|
+
: newValue;
|
|
245
|
+
field.onChange(serializedValue);
|
|
246
|
+
}, [field.onChange, fieldDef, valueMode]);
|
|
247
|
+
|
|
248
|
+
// Pre-define all change handlers at the top level to follow Rules of Hooks
|
|
249
|
+
const handleNumericChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
250
|
+
const val = e.target.valueAsNumber;
|
|
251
|
+
handleChange(isNaN(val) ? (valueMode === 'json-string' ? '' : undefined) : val);
|
|
252
|
+
}, [handleChange, valueMode]);
|
|
253
|
+
|
|
254
|
+
const handleRegularNumericChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
255
|
+
const val = e.target.valueAsNumber;
|
|
256
|
+
handleChange(isNaN(val) ? undefined : val);
|
|
257
|
+
}, [handleChange]);
|
|
258
|
+
|
|
259
|
+
const handleTextareaChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
260
|
+
handleChange(e.target.value);
|
|
261
|
+
}, [handleChange]);
|
|
262
|
+
|
|
263
|
+
const handleTextChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
264
|
+
handleChange(e.target.value);
|
|
265
|
+
}, [handleChange]);
|
|
266
|
+
|
|
267
|
+
// Create props object for all renderers
|
|
268
|
+
const rendererProps: ComponentRendererProps = {
|
|
269
|
+
fieldDef,
|
|
270
|
+
field,
|
|
271
|
+
valueMode,
|
|
272
|
+
isReadonly,
|
|
273
|
+
transformedValue,
|
|
274
|
+
handleChange,
|
|
275
|
+
handleNumericChange,
|
|
276
|
+
handleRegularNumericChange,
|
|
277
|
+
handleTextareaChange,
|
|
278
|
+
handleTextChange,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Try each renderer in order, return the first match
|
|
282
|
+
return (
|
|
283
|
+
renderRelationInput(rendererProps) ||
|
|
284
|
+
renderSelectInput(rendererProps) ||
|
|
285
|
+
renderNumericInput(rendererProps) ||
|
|
286
|
+
renderBooleanInput(rendererProps) ||
|
|
287
|
+
renderDateTimeInput(rendererProps) ||
|
|
288
|
+
renderTextareaInput(rendererProps) ||
|
|
289
|
+
renderTextInput(rendererProps)
|
|
290
|
+
);
|
|
291
|
+
}
|