@vendure/dashboard 3.4.1-master-202508050244 → 3.4.1-master-202508080243

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.
@@ -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, ControllerRenderProps } from 'react-hook-form';
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
- <FormInputForType fieldDef={fieldDef} field={field} />
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
- <FormInputForType fieldDef={fieldDef} field={field} />
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
+ }