@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.
@@ -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
+ }