@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
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { CustomFieldListInput } from '@/vdb/components/data-input/custom-field-list-input.js';
|
|
2
|
-
import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
|
|
3
|
-
import { DefaultRelationInput } from '@/vdb/components/data-input/default-relation-input.js';
|
|
4
|
-
import { SelectWithOptions } from '@/vdb/components/data-input/select-with-options.js';
|
|
5
2
|
import { StructFormInput } from '@/vdb/components/data-input/struct-form-input.js';
|
|
6
3
|
import {
|
|
7
4
|
FormControl,
|
|
@@ -11,20 +8,18 @@ import {
|
|
|
11
8
|
FormLabel,
|
|
12
9
|
FormMessage,
|
|
13
10
|
} from '@/vdb/components/ui/form.js';
|
|
14
|
-
import { Input } from '@/vdb/components/ui/input.js';
|
|
15
11
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/vdb/components/ui/tabs.js';
|
|
16
12
|
import { CustomFormComponent } from '@/vdb/framework/form-engine/custom-form-component.js';
|
|
17
13
|
import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
|
|
18
14
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
19
15
|
import { useLingui } from '@/vdb/lib/trans.js';
|
|
20
16
|
import { customFieldConfigFragment } from '@/vdb/providers/server-config.js';
|
|
21
|
-
import { StringCustomFieldConfig } from '@vendure/common/lib/generated-types';
|
|
22
|
-
import { CustomFieldType } from '@vendure/common/lib/shared-types';
|
|
23
17
|
import { ResultOf } from 'gql.tada';
|
|
24
18
|
import React, { useMemo } from 'react';
|
|
25
|
-
import { Control
|
|
26
|
-
import { Switch } from '../ui/switch.js';
|
|
19
|
+
import { Control } from 'react-hook-form';
|
|
27
20
|
import { TranslatableFormField } from './translatable-form-field.js';
|
|
21
|
+
import { customFieldToUniversal } from './universal-field-definition.js';
|
|
22
|
+
import { UniversalFormInput } from './universal-form-input.js';
|
|
28
23
|
|
|
29
24
|
type CustomFieldConfig = ResultOf<typeof customFieldConfigFragment>;
|
|
30
25
|
|
|
@@ -174,7 +169,14 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
|
|
|
174
169
|
}}
|
|
175
170
|
/>
|
|
176
171
|
) : (
|
|
177
|
-
<
|
|
172
|
+
<UniversalFormInput
|
|
173
|
+
fieldDef={customFieldToUniversal(fieldDef)}
|
|
174
|
+
field={field}
|
|
175
|
+
valueMode="native"
|
|
176
|
+
disabled={fieldDef.readonly ?? false}
|
|
177
|
+
control={control}
|
|
178
|
+
getTranslation={getTranslation}
|
|
179
|
+
/>
|
|
178
180
|
)}
|
|
179
181
|
</FormControl>
|
|
180
182
|
<FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
|
|
@@ -293,7 +295,14 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
|
|
|
293
295
|
getTranslation={getTranslation}
|
|
294
296
|
fieldName={fieldDef.name}
|
|
295
297
|
>
|
|
296
|
-
<
|
|
298
|
+
<UniversalFormInput
|
|
299
|
+
fieldDef={customFieldToUniversal(fieldDef)}
|
|
300
|
+
field={field}
|
|
301
|
+
valueMode="native"
|
|
302
|
+
disabled={fieldDef.readonly ?? false}
|
|
303
|
+
control={control}
|
|
304
|
+
getTranslation={getTranslation}
|
|
305
|
+
/>
|
|
297
306
|
</CustomFieldFormItem>
|
|
298
307
|
)}
|
|
299
308
|
/>
|
|
@@ -325,136 +334,3 @@ function CustomFieldFormItem({
|
|
|
325
334
|
</FormItem>
|
|
326
335
|
);
|
|
327
336
|
}
|
|
328
|
-
|
|
329
|
-
function FormInputForType({
|
|
330
|
-
fieldDef,
|
|
331
|
-
field,
|
|
332
|
-
}: Readonly<{
|
|
333
|
-
fieldDef: CustomFieldConfig;
|
|
334
|
-
field: ControllerRenderProps<any, any>;
|
|
335
|
-
}>) {
|
|
336
|
-
const isReadonly = fieldDef.readonly ?? false;
|
|
337
|
-
const isList = fieldDef.list ?? false;
|
|
338
|
-
|
|
339
|
-
// Helper function to render individual input components
|
|
340
|
-
const renderSingleInput = (inputField: ControllerRenderProps<any, any>) => {
|
|
341
|
-
switch (fieldDef.type as CustomFieldType) {
|
|
342
|
-
case 'float':
|
|
343
|
-
case 'int': {
|
|
344
|
-
const numericFieldDef = fieldDef as any;
|
|
345
|
-
const isFloat = fieldDef.type === 'float';
|
|
346
|
-
const min = isFloat ? numericFieldDef.floatMin : numericFieldDef.intMin;
|
|
347
|
-
const max = isFloat ? numericFieldDef.floatMax : numericFieldDef.intMax;
|
|
348
|
-
const step = isFloat ? numericFieldDef.floatStep : numericFieldDef.intStep;
|
|
349
|
-
|
|
350
|
-
return (
|
|
351
|
-
<Input
|
|
352
|
-
type="number"
|
|
353
|
-
value={inputField.value ?? ''}
|
|
354
|
-
onChange={e => {
|
|
355
|
-
const value = e.target.valueAsNumber;
|
|
356
|
-
inputField.onChange(isNaN(value) ? undefined : value);
|
|
357
|
-
}}
|
|
358
|
-
onBlur={inputField.onBlur}
|
|
359
|
-
name={inputField.name}
|
|
360
|
-
disabled={isReadonly}
|
|
361
|
-
min={min}
|
|
362
|
-
max={max}
|
|
363
|
-
step={step}
|
|
364
|
-
/>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
case 'boolean':
|
|
368
|
-
return (
|
|
369
|
-
<Switch
|
|
370
|
-
checked={inputField.value}
|
|
371
|
-
onCheckedChange={inputField.onChange}
|
|
372
|
-
disabled={isReadonly}
|
|
373
|
-
/>
|
|
374
|
-
);
|
|
375
|
-
case 'datetime': {
|
|
376
|
-
return (
|
|
377
|
-
<DateTimeInput
|
|
378
|
-
value={inputField.value}
|
|
379
|
-
onChange={inputField.onChange}
|
|
380
|
-
disabled={isReadonly}
|
|
381
|
-
/>
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
case 'struct':
|
|
385
|
-
// Struct fields need special handling and can't be rendered as simple inputs
|
|
386
|
-
return null;
|
|
387
|
-
case 'string':
|
|
388
|
-
default:
|
|
389
|
-
return (
|
|
390
|
-
<Input
|
|
391
|
-
value={inputField.value ?? ''}
|
|
392
|
-
onChange={e => inputField.onChange(e.target.value)}
|
|
393
|
-
onBlur={inputField.onBlur}
|
|
394
|
-
name={inputField.name}
|
|
395
|
-
disabled={isReadonly}
|
|
396
|
-
/>
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
// Handle struct fields with special component
|
|
402
|
-
if (fieldDef.type === 'struct') {
|
|
403
|
-
// We need access to the control and getTranslation function
|
|
404
|
-
// This will need to be passed down from the parent component
|
|
405
|
-
return null; // Placeholder - struct fields are handled differently in the parent
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Handle relation fields directly (they handle list/single internally)
|
|
409
|
-
if (fieldDef.type === 'relation') {
|
|
410
|
-
return <DefaultRelationInput fieldDef={fieldDef as any} field={field} disabled={isReadonly} />;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Handle string fields with options (dropdown) - already handles list case with multi-select
|
|
414
|
-
if (fieldDef.type === 'string') {
|
|
415
|
-
const options = (fieldDef as StringCustomFieldConfig).options;
|
|
416
|
-
if (options && options.length > 0) {
|
|
417
|
-
return (
|
|
418
|
-
<SelectWithOptions
|
|
419
|
-
field={field}
|
|
420
|
-
options={options}
|
|
421
|
-
disabled={isReadonly}
|
|
422
|
-
isListField={isList}
|
|
423
|
-
/>
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// For list fields (except string with options and relations which are handled above), wrap with list input
|
|
429
|
-
if (isList) {
|
|
430
|
-
const getDefaultValue = () => {
|
|
431
|
-
switch (fieldDef.type as CustomFieldType) {
|
|
432
|
-
case 'string':
|
|
433
|
-
return '';
|
|
434
|
-
case 'int':
|
|
435
|
-
case 'float':
|
|
436
|
-
return 0;
|
|
437
|
-
case 'boolean':
|
|
438
|
-
return false;
|
|
439
|
-
case 'datetime':
|
|
440
|
-
return '';
|
|
441
|
-
case 'relation':
|
|
442
|
-
return '';
|
|
443
|
-
default:
|
|
444
|
-
return '';
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
return (
|
|
449
|
-
<CustomFieldListInput
|
|
450
|
-
field={field}
|
|
451
|
-
disabled={isReadonly}
|
|
452
|
-
renderInput={(index, inputField) => renderSingleInput(inputField)}
|
|
453
|
-
defaultValue={getDefaultValue()}
|
|
454
|
-
/>
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// For non-list fields, render directly
|
|
459
|
-
return renderSingleInput(field);
|
|
460
|
-
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { DefaultFormComponentId } from '@vendure/common/lib/shared-types';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { AffixedInput } from '@/vdb/components/data-input/affixed-input.js';
|
|
5
|
+
import { CombinationModeInput } from '@/vdb/components/data-input/combination-mode-input.js';
|
|
6
|
+
import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
|
|
7
|
+
import { DefaultRelationInput } from '@/vdb/components/data-input/default-relation-input.js';
|
|
8
|
+
import { FacetValueInput } from '@/vdb/components/data-input/facet-value-input.js';
|
|
9
|
+
import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
|
|
10
|
+
import { ProductMultiInput } from '@/vdb/components/data-input/product-multi-selector.js';
|
|
11
|
+
import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
|
|
12
|
+
import { Input } from '@/vdb/components/ui/input.js';
|
|
13
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
|
|
14
|
+
import { Switch } from '@/vdb/components/ui/switch.js';
|
|
15
|
+
import { Textarea } from '@/vdb/components/ui/textarea.js';
|
|
16
|
+
|
|
17
|
+
import { UniversalFieldDefinition } from './universal-field-definition.js';
|
|
18
|
+
import { transformValue, ValueMode } from './value-transformers.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom hook to handle value transformation between native and JSON string modes
|
|
22
|
+
* Eliminates duplication across form input components
|
|
23
|
+
*/
|
|
24
|
+
function useValueTransformation(
|
|
25
|
+
field: { value: any; onChange: (value: any) => void },
|
|
26
|
+
fieldDef: UniversalFieldDefinition,
|
|
27
|
+
valueMode: ValueMode,
|
|
28
|
+
) {
|
|
29
|
+
const transformedValue = React.useMemo(() => {
|
|
30
|
+
return valueMode === 'json-string'
|
|
31
|
+
? transformValue(field.value, fieldDef, valueMode, 'parse')
|
|
32
|
+
: field.value;
|
|
33
|
+
}, [field.value, fieldDef, valueMode]);
|
|
34
|
+
|
|
35
|
+
const handleChange = React.useCallback(
|
|
36
|
+
(newValue: any) => {
|
|
37
|
+
const serializedValue =
|
|
38
|
+
valueMode === 'json-string'
|
|
39
|
+
? transformValue(newValue, fieldDef, valueMode, 'serialize')
|
|
40
|
+
: newValue;
|
|
41
|
+
field.onChange(serializedValue);
|
|
42
|
+
},
|
|
43
|
+
[field.onChange, fieldDef, valueMode],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return { transformedValue, handleChange };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DirectFormComponentProps {
|
|
50
|
+
fieldDef: UniversalFieldDefinition;
|
|
51
|
+
field: {
|
|
52
|
+
value: any;
|
|
53
|
+
onChange: (value: any) => void;
|
|
54
|
+
onBlur?: () => void;
|
|
55
|
+
name: string;
|
|
56
|
+
ref?: any;
|
|
57
|
+
};
|
|
58
|
+
valueMode: ValueMode;
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Text input wrapper for config args
|
|
64
|
+
*/
|
|
65
|
+
const TextFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
66
|
+
const handleChange = React.useCallback(
|
|
67
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
68
|
+
// For both modes, text values are stored as strings
|
|
69
|
+
field.onChange(e.target.value);
|
|
70
|
+
},
|
|
71
|
+
[field.onChange],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const value = field.value || '';
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Input
|
|
78
|
+
type="text"
|
|
79
|
+
value={value}
|
|
80
|
+
onChange={handleChange}
|
|
81
|
+
onBlur={field.onBlur}
|
|
82
|
+
name={field.name}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
className={valueMode === 'json-string' ? 'bg-background' : undefined}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Number input wrapper for config args
|
|
91
|
+
*/
|
|
92
|
+
const NumberFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
93
|
+
const ui = fieldDef.ui;
|
|
94
|
+
const isFloat = fieldDef.type === 'float';
|
|
95
|
+
const min = ui?.min;
|
|
96
|
+
const max = ui?.max;
|
|
97
|
+
const step = ui?.step || (isFloat ? 0.01 : 1);
|
|
98
|
+
const prefix = ui?.prefix;
|
|
99
|
+
const suffix = ui?.suffix;
|
|
100
|
+
|
|
101
|
+
const handleChange = React.useCallback(
|
|
102
|
+
(newValue: number | '') => {
|
|
103
|
+
if (valueMode === 'json-string') {
|
|
104
|
+
// For config args, store as string
|
|
105
|
+
field.onChange(newValue === '' ? '' : newValue.toString());
|
|
106
|
+
} else {
|
|
107
|
+
// For custom fields, store as number or undefined
|
|
108
|
+
field.onChange(newValue === '' ? undefined : newValue);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[field.onChange, valueMode],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Parse current value to number
|
|
115
|
+
const numericValue = React.useMemo(() => {
|
|
116
|
+
if (field.value === undefined || field.value === null || field.value === '') {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
const parsed = typeof field.value === 'number' ? field.value : parseFloat(field.value);
|
|
120
|
+
return isNaN(parsed) ? '' : parsed;
|
|
121
|
+
}, [field.value]);
|
|
122
|
+
|
|
123
|
+
// Use AffixedInput if we have prefix/suffix or for config args mode
|
|
124
|
+
if (prefix || suffix || valueMode === 'json-string') {
|
|
125
|
+
return (
|
|
126
|
+
<AffixedInput
|
|
127
|
+
type="number"
|
|
128
|
+
value={numericValue}
|
|
129
|
+
onChange={e => {
|
|
130
|
+
const val = e.target.valueAsNumber;
|
|
131
|
+
handleChange(isNaN(val) ? '' : val);
|
|
132
|
+
}}
|
|
133
|
+
disabled={disabled}
|
|
134
|
+
min={min}
|
|
135
|
+
max={max}
|
|
136
|
+
step={step}
|
|
137
|
+
prefix={prefix}
|
|
138
|
+
suffix={suffix}
|
|
139
|
+
className="bg-background"
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Input
|
|
146
|
+
type="number"
|
|
147
|
+
value={numericValue}
|
|
148
|
+
onChange={e => {
|
|
149
|
+
const val = e.target.valueAsNumber;
|
|
150
|
+
handleChange(isNaN(val) ? '' : val);
|
|
151
|
+
}}
|
|
152
|
+
onBlur={field.onBlur}
|
|
153
|
+
name={field.name}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
min={min}
|
|
156
|
+
max={max}
|
|
157
|
+
step={step}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Boolean input wrapper
|
|
164
|
+
*/
|
|
165
|
+
const BooleanFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
166
|
+
// Parse the current value to boolean
|
|
167
|
+
const currentValue = React.useMemo(() => {
|
|
168
|
+
if (valueMode === 'json-string') {
|
|
169
|
+
return field.value === 'true' || field.value === true;
|
|
170
|
+
} else {
|
|
171
|
+
return Boolean(field.value);
|
|
172
|
+
}
|
|
173
|
+
}, [field.value, valueMode]);
|
|
174
|
+
|
|
175
|
+
// Simple change handler - directly call field.onChange
|
|
176
|
+
const handleChange = React.useCallback(
|
|
177
|
+
(newValue: boolean) => {
|
|
178
|
+
if (valueMode === 'json-string') {
|
|
179
|
+
field.onChange(newValue.toString());
|
|
180
|
+
} else {
|
|
181
|
+
field.onChange(newValue);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
[field.onChange, valueMode],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return <Switch checked={currentValue} onCheckedChange={handleChange} disabled={disabled} />;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Currency input wrapper (uses MoneyInput)
|
|
192
|
+
*/
|
|
193
|
+
const CurrencyFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
194
|
+
const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
|
|
195
|
+
|
|
196
|
+
return <MoneyInput value={transformedValue} onChange={handleChange} disabled={disabled} />;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Date input wrapper
|
|
201
|
+
*/
|
|
202
|
+
const DateFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
203
|
+
const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
|
|
204
|
+
|
|
205
|
+
return <DateTimeInput value={transformedValue} onChange={handleChange} disabled={disabled} />;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Select input wrapper
|
|
210
|
+
*/
|
|
211
|
+
const SelectFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
212
|
+
const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
|
|
213
|
+
const options = fieldDef.ui?.options || [];
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Select value={transformedValue || ''} onValueChange={handleChange} disabled={disabled}>
|
|
217
|
+
<SelectTrigger className="bg-background mb-0">
|
|
218
|
+
<SelectValue placeholder="Select an option..." />
|
|
219
|
+
</SelectTrigger>
|
|
220
|
+
<SelectContent>
|
|
221
|
+
{options.map(option => (
|
|
222
|
+
<SelectItem key={option.value} value={option.value}>
|
|
223
|
+
{typeof option.label === 'string'
|
|
224
|
+
? option.label
|
|
225
|
+
: Array.isArray(option.label)
|
|
226
|
+
? option.label[0]?.value || option.value
|
|
227
|
+
: option.value}
|
|
228
|
+
</SelectItem>
|
|
229
|
+
))}
|
|
230
|
+
</SelectContent>
|
|
231
|
+
</Select>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Textarea input wrapper
|
|
237
|
+
*/
|
|
238
|
+
const TextareaFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
239
|
+
const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
|
|
240
|
+
|
|
241
|
+
const handleTextareaChange = React.useCallback(
|
|
242
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
243
|
+
handleChange(e.target.value);
|
|
244
|
+
},
|
|
245
|
+
[handleChange],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<Textarea
|
|
250
|
+
value={transformedValue || ''}
|
|
251
|
+
onChange={handleTextareaChange}
|
|
252
|
+
disabled={disabled}
|
|
253
|
+
spellCheck={fieldDef.ui?.spellcheck ?? true}
|
|
254
|
+
placeholder="Enter text..."
|
|
255
|
+
rows={4}
|
|
256
|
+
className="bg-background"
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Product selector wrapper (uses DefaultRelationInput)
|
|
263
|
+
*/
|
|
264
|
+
const ProductSelectorFormInput: React.FC<DirectFormComponentProps> = ({
|
|
265
|
+
field,
|
|
266
|
+
disabled,
|
|
267
|
+
fieldDef,
|
|
268
|
+
valueMode,
|
|
269
|
+
}) => {
|
|
270
|
+
const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
|
|
271
|
+
const entityType = fieldDef.ui?.selectionMode === 'variant' ? 'ProductVariant' : 'Product';
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<DefaultRelationInput
|
|
275
|
+
fieldDef={
|
|
276
|
+
{
|
|
277
|
+
entity: entityType,
|
|
278
|
+
list: fieldDef.list,
|
|
279
|
+
} as any
|
|
280
|
+
}
|
|
281
|
+
field={{
|
|
282
|
+
...field,
|
|
283
|
+
value: transformedValue,
|
|
284
|
+
onChange: handleChange,
|
|
285
|
+
}}
|
|
286
|
+
disabled={disabled}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Customer group input wrapper
|
|
293
|
+
*/
|
|
294
|
+
const CustomerGroupFormInput: React.FC<DirectFormComponentProps> = ({
|
|
295
|
+
field,
|
|
296
|
+
disabled,
|
|
297
|
+
fieldDef,
|
|
298
|
+
valueMode,
|
|
299
|
+
}) => {
|
|
300
|
+
const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<DefaultRelationInput
|
|
304
|
+
fieldDef={
|
|
305
|
+
{
|
|
306
|
+
entity: 'CustomerGroup',
|
|
307
|
+
list: fieldDef.list,
|
|
308
|
+
} as any
|
|
309
|
+
}
|
|
310
|
+
field={{
|
|
311
|
+
...field,
|
|
312
|
+
value: transformedValue,
|
|
313
|
+
onChange: handleChange,
|
|
314
|
+
}}
|
|
315
|
+
disabled={disabled}
|
|
316
|
+
/>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Password input wrapper (uses regular Input with type="password")
|
|
322
|
+
*/
|
|
323
|
+
const PasswordFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
|
|
324
|
+
const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
|
|
325
|
+
|
|
326
|
+
const handleInputChange = React.useCallback(
|
|
327
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
328
|
+
handleChange(e.target.value);
|
|
329
|
+
},
|
|
330
|
+
[handleChange],
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<Input
|
|
335
|
+
type="password"
|
|
336
|
+
value={transformedValue || ''}
|
|
337
|
+
onChange={handleInputChange}
|
|
338
|
+
onBlur={field.onBlur}
|
|
339
|
+
name={field.name}
|
|
340
|
+
disabled={disabled}
|
|
341
|
+
className={valueMode === 'json-string' ? 'bg-background' : undefined}
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Direct mapping from DefaultFormComponentId to React components
|
|
348
|
+
* This eliminates the need for intermediate registry IDs
|
|
349
|
+
*/
|
|
350
|
+
export const DIRECT_FORM_COMPONENT_MAP: Record<DefaultFormComponentId, React.FC<DirectFormComponentProps>> = {
|
|
351
|
+
'boolean-form-input': BooleanFormInput,
|
|
352
|
+
'currency-form-input': CurrencyFormInput,
|
|
353
|
+
'customer-group-form-input': CustomerGroupFormInput,
|
|
354
|
+
'date-form-input': DateFormInput,
|
|
355
|
+
'facet-value-form-input': ({ field, disabled }) => (
|
|
356
|
+
<FacetValueInput value={field.value} onChange={field.onChange} readOnly={disabled} />
|
|
357
|
+
),
|
|
358
|
+
'json-editor-form-input': TextareaFormInput, // Fallback to textarea for now
|
|
359
|
+
'html-editor-form-input': ({ field, disabled }) => (
|
|
360
|
+
<RichTextInput value={field.value} onChange={field.onChange} disabled={disabled} />
|
|
361
|
+
),
|
|
362
|
+
'number-form-input': NumberFormInput,
|
|
363
|
+
'password-form-input': PasswordFormInput,
|
|
364
|
+
'product-selector-form-input': ProductSelectorFormInput,
|
|
365
|
+
'relation-form-input': ProductSelectorFormInput, // Uses same relation logic
|
|
366
|
+
'rich-text-form-input': ({ field, disabled }) => (
|
|
367
|
+
<RichTextInput value={field.value} onChange={field.onChange} disabled={disabled} />
|
|
368
|
+
),
|
|
369
|
+
'select-form-input': SelectFormInput,
|
|
370
|
+
'text-form-input': TextFormInput,
|
|
371
|
+
'textarea-form-input': TextareaFormInput,
|
|
372
|
+
'product-multi-form-input': ({ field, disabled, fieldDef }) => (
|
|
373
|
+
<ProductMultiInput
|
|
374
|
+
value={field.value}
|
|
375
|
+
onChange={field.onChange}
|
|
376
|
+
disabled={disabled}
|
|
377
|
+
selectionMode={fieldDef.ui?.selectionMode as any}
|
|
378
|
+
/>
|
|
379
|
+
),
|
|
380
|
+
'combination-mode-form-input': ({ field, disabled }) => (
|
|
381
|
+
<CombinationModeInput value={field.value} onChange={field.onChange} disabled={disabled} />
|
|
382
|
+
),
|
|
383
|
+
'struct-form-input': TextareaFormInput, // Fallback for now
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get a direct form component by ID
|
|
388
|
+
*/
|
|
389
|
+
export function getDirectFormComponent(
|
|
390
|
+
componentId: DefaultFormComponentId,
|
|
391
|
+
): React.FC<DirectFormComponentProps> | undefined {
|
|
392
|
+
return DIRECT_FORM_COMPONENT_MAP[componentId];
|
|
393
|
+
}
|