@vendure/dashboard 3.3.8-master-202507290247 → 3.3.8-master-202507300243

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 (35) hide show
  1. package/package.json +4 -4
  2. package/src/app/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx +1 -1
  3. package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +11 -78
  4. package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +11 -81
  5. package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +10 -77
  6. package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +12 -87
  7. package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +12 -87
  8. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +10 -80
  9. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +10 -79
  10. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +8 -6
  11. package/src/lib/components/data-input/combination-mode-input.tsx +52 -0
  12. package/src/lib/components/data-input/configurable-operation-list-input.tsx +433 -0
  13. package/src/lib/components/data-input/custom-field-list-input.tsx +297 -0
  14. package/src/lib/components/data-input/datetime-input.tsx +5 -2
  15. package/src/lib/components/data-input/default-relation-input.tsx +599 -0
  16. package/src/lib/components/data-input/index.ts +6 -0
  17. package/src/lib/components/data-input/product-multi-selector.tsx +426 -0
  18. package/src/lib/components/data-input/relation-selector.tsx +7 -6
  19. package/src/lib/components/data-input/select-with-options.tsx +84 -0
  20. package/src/lib/components/data-input/struct-form-input.tsx +324 -0
  21. package/src/lib/components/shared/configurable-operation-arg-input.tsx +365 -21
  22. package/src/lib/components/shared/configurable-operation-input.tsx +81 -41
  23. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +260 -0
  24. package/src/lib/components/shared/configurable-operation-selector.tsx +156 -0
  25. package/src/lib/components/shared/custom-fields-form.tsx +207 -36
  26. package/src/lib/components/shared/multi-select.tsx +1 -1
  27. package/src/lib/components/ui/form.tsx +4 -4
  28. package/src/lib/framework/extension-api/input-component-extensions.tsx +5 -1
  29. package/src/lib/framework/form-engine/form-schema-tools.spec.ts +472 -0
  30. package/src/lib/framework/form-engine/form-schema-tools.ts +340 -5
  31. package/src/lib/framework/form-engine/use-generated-form.tsx +24 -8
  32. package/src/lib/framework/form-engine/utils.ts +3 -9
  33. package/src/lib/framework/layout-engine/page-layout.tsx +11 -3
  34. package/src/lib/framework/page/use-detail-page.ts +3 -3
  35. package/src/lib/lib/utils.ts +26 -24
@@ -1,51 +1,395 @@
1
1
  import { InputComponent } from '@/vdb/framework/component-registry/dynamic-component.js';
2
2
  import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
3
+ import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types';
3
4
  import { ConfigArgType } from '@vendure/core';
5
+ import { AffixedInput } from '../data-input/affixed-input.js';
6
+ import { ConfigurableOperationListInput } from '../data-input/configurable-operation-list-input.js';
7
+ import { DateTimeInput } from '../data-input/datetime-input.js';
8
+ import { DefaultRelationInput } from '../data-input/default-relation-input.js';
4
9
  import { FacetValueInput } from '../data-input/facet-value-input.js';
10
+ import { Input } from '../ui/input.js';
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
12
+ import { Switch } from '../ui/switch.js';
13
+ import { Textarea } from '../ui/textarea.js';
5
14
 
6
15
  export interface ConfigurableOperationArgInputProps {
7
16
  definition: ConfigurableOperationDefFragment['args'][number];
8
17
  readOnly?: boolean;
9
18
  value: string;
10
19
  onChange: (value: any) => void;
20
+ position?: number;
11
21
  }
12
22
 
23
+ /**
24
+ * Maps Vendure UI component names to their corresponding Dashboard input component IDs
25
+ */
26
+ const UI_COMPONENT_MAP = {
27
+ 'number-form-input': 'vendure:numberInput',
28
+ 'currency-form-input': 'vendure:currencyInput',
29
+ 'facet-value-form-input': 'facet-value-input',
30
+ 'product-selector-form-input': 'vendure:productSelectorInput',
31
+ 'customer-group-form-input': 'vendure:customerGroupInput',
32
+ 'date-form-input': 'date-input',
33
+ 'textarea-form-input': 'textarea-input',
34
+ 'password-form-input': 'vendure:passwordInput',
35
+ 'json-editor-form-input': 'vendure:jsonEditorInput',
36
+ 'html-editor-form-input': 'vendure:htmlEditorInput',
37
+ 'rich-text-form-input': 'vendure:richTextInput',
38
+ 'boolean-form-input': 'boolean-input',
39
+ 'select-form-input': 'select-input',
40
+ 'text-form-input': 'vendure:textInput',
41
+ 'product-multi-form-input': 'vendure:productMultiInput',
42
+ 'combination-mode-form-input': 'vendure:combinationModeInput',
43
+ 'relation-form-input': 'vendure:relationInput',
44
+ 'struct-form-input': 'vendure:structInput',
45
+ } as const;
46
+
13
47
  export function ConfigurableOperationArgInput({
14
48
  definition,
15
49
  value,
16
50
  onChange,
17
51
  readOnly,
18
- }: ConfigurableOperationArgInputProps) {
19
- if ((definition.ui as any)?.component === 'facet-value-form-input') {
20
- return <FacetValueInput value={value} onChange={onChange} readOnly={readOnly} />;
52
+ position,
53
+ }: Readonly<ConfigurableOperationArgInputProps>) {
54
+ const uiComponent = (definition.ui as any)?.component;
55
+ const argType = definition.type as ConfigArgType;
56
+ const isList = definition.list ?? false;
57
+
58
+ // Handle specific UI components first
59
+ if (uiComponent) {
60
+ switch (uiComponent) {
61
+ case 'product-selector-form-input': {
62
+ const entityType =
63
+ (definition.ui as any)?.selectionMode === 'variant' ? 'ProductVariant' : 'Product';
64
+ const isMultiple = (definition.ui as any)?.multiple ?? false;
65
+ return (
66
+ <DefaultRelationInput
67
+ fieldDef={
68
+ {
69
+ entity: entityType,
70
+ list: isMultiple,
71
+ } as RelationCustomFieldConfig
72
+ }
73
+ field={{
74
+ value,
75
+ onChange,
76
+ onBlur: () => {},
77
+ name: '',
78
+ ref: () => {},
79
+ }}
80
+ disabled={readOnly}
81
+ />
82
+ );
83
+ }
84
+ case 'customer-group-form-input': {
85
+ const isCustomerGroupMultiple = (definition.ui as any)?.multiple ?? false;
86
+ return (
87
+ <DefaultRelationInput
88
+ fieldDef={
89
+ {
90
+ entity: 'CustomerGroup',
91
+ list: isCustomerGroupMultiple,
92
+ } as RelationCustomFieldConfig
93
+ }
94
+ field={{
95
+ value,
96
+ onChange,
97
+ onBlur: () => {},
98
+ name: '',
99
+ ref: () => {},
100
+ }}
101
+ disabled={readOnly}
102
+ />
103
+ );
104
+ }
105
+ case 'facet-value-form-input': {
106
+ return <FacetValueInput value={value} onChange={onChange} readOnly={readOnly} />;
107
+ }
108
+ case 'select-form-input': {
109
+ return (
110
+ <SelectInput
111
+ definition={definition}
112
+ value={value}
113
+ onChange={onChange}
114
+ readOnly={readOnly}
115
+ />
116
+ );
117
+ }
118
+ case 'textarea-form-input': {
119
+ return (
120
+ <TextareaInput
121
+ definition={definition}
122
+ value={value}
123
+ onChange={onChange}
124
+ readOnly={readOnly}
125
+ />
126
+ );
127
+ }
128
+ case 'date-form-input': {
129
+ return <DateTimeInput value={value} onChange={onChange} disabled={readOnly} />;
130
+ }
131
+ case 'boolean-form-input': {
132
+ return <BooleanInput value={value} onChange={onChange} readOnly={readOnly} />;
133
+ }
134
+ case 'number-form-input': {
135
+ return (
136
+ <NumberInput
137
+ definition={definition}
138
+ value={value}
139
+ onChange={onChange}
140
+ readOnly={readOnly}
141
+ />
142
+ );
143
+ }
144
+ case 'currency-form-input': {
145
+ return (
146
+ <CurrencyInput
147
+ definition={definition}
148
+ value={value}
149
+ onChange={onChange}
150
+ readOnly={readOnly}
151
+ />
152
+ );
153
+ }
154
+ default: {
155
+ // Try to use the component registry for other UI components
156
+ const componentId = UI_COMPONENT_MAP[uiComponent as keyof typeof UI_COMPONENT_MAP];
157
+ if (componentId) {
158
+ try {
159
+ return (
160
+ <InputComponent
161
+ id={componentId}
162
+ value={value}
163
+ onChange={onChange}
164
+ readOnly={readOnly}
165
+ position={position}
166
+ definition={definition}
167
+ {...(definition.ui as any)}
168
+ />
169
+ );
170
+ } catch (error) {
171
+ console.warn(
172
+ `Failed to load UI component ${uiComponent}, falling back to type-based input`,
173
+ );
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // Handle list fields with array wrapper
181
+ if (isList) {
182
+ return (
183
+ <ConfigurableOperationListInput
184
+ definition={definition}
185
+ value={value}
186
+ onChange={onChange}
187
+ readOnly={readOnly}
188
+ />
189
+ );
21
190
  }
22
- switch (definition.type as ConfigArgType) {
191
+
192
+ // Fall back to type-based rendering
193
+ switch (argType) {
23
194
  case 'boolean':
195
+ return <BooleanInput value={value} onChange={onChange} readOnly={readOnly} />;
196
+
197
+ case 'int':
198
+ case 'float':
24
199
  return (
25
- <InputComponent
26
- id="vendure:checkboxInput"
27
- value={value}
28
- onChange={(value: any) => onChange(value)}
29
- readOnly={readOnly}
30
- />
200
+ <NumberInput definition={definition} value={value} onChange={onChange} readOnly={readOnly} />
31
201
  );
32
- case 'string':
202
+
203
+ case 'datetime':
204
+ return <DateTimeInput value={value} onChange={onChange} disabled={readOnly} />;
205
+
206
+ case 'ID':
207
+ // ID fields typically need specialized selectors
33
208
  return (
34
- <InputComponent
35
- id="vendure:textInput"
36
- value={value}
37
- onChange={(value: any) => onChange(value)}
38
- readOnly={readOnly}
209
+ <Input
210
+ type="text"
211
+ value={value || ''}
212
+ onChange={e => onChange(e.target.value)}
213
+ disabled={readOnly}
214
+ placeholder="Enter ID..."
215
+ className="bg-background"
39
216
  />
40
217
  );
218
+
219
+ case 'string':
41
220
  default:
42
221
  return (
43
- <InputComponent
44
- id="vendure:textInput"
45
- value={value}
46
- onChange={(value: any) => onChange(value)}
47
- readOnly={readOnly}
222
+ <Input
223
+ type="text"
224
+ value={value || ''}
225
+ onChange={e => onChange(e.target.value)}
226
+ disabled={readOnly}
227
+ className="bg-background"
48
228
  />
49
229
  );
50
230
  }
51
231
  }
232
+
233
+ /**
234
+ * Boolean input component
235
+ */
236
+ function BooleanInput({
237
+ value,
238
+ onChange,
239
+ readOnly,
240
+ }: Readonly<{
241
+ value: string;
242
+ onChange: (value: string) => void;
243
+ readOnly?: boolean;
244
+ }>) {
245
+ const boolValue = value === 'true';
246
+
247
+ return (
248
+ <Switch
249
+ checked={boolValue}
250
+ onCheckedChange={checked => onChange(checked.toString())}
251
+ disabled={readOnly}
252
+ />
253
+ );
254
+ }
255
+
256
+ /**
257
+ * Number input component with support for UI configuration
258
+ */
259
+ function NumberInput({
260
+ definition,
261
+ value,
262
+ onChange,
263
+ readOnly,
264
+ }: Readonly<{
265
+ definition: ConfigurableOperationDefFragment['args'][number];
266
+ value: string;
267
+ onChange: (value: string) => void;
268
+ readOnly?: boolean;
269
+ }>) {
270
+ const ui = definition.ui as any;
271
+ const isFloat = (definition.type as ConfigArgType) === 'float';
272
+ const min = ui?.min;
273
+ const max = ui?.max;
274
+ const step = ui?.step || (isFloat ? 0.01 : 1);
275
+ const prefix = ui?.prefix;
276
+ const suffix = ui?.suffix;
277
+
278
+ const numericValue = value ? parseFloat(value) : '';
279
+
280
+ return (
281
+ <AffixedInput
282
+ type="number"
283
+ value={numericValue}
284
+ onChange={e => {
285
+ const val = e.target.valueAsNumber;
286
+ onChange(isNaN(val) ? '' : val.toString());
287
+ }}
288
+ disabled={readOnly}
289
+ min={min}
290
+ max={max}
291
+ step={step}
292
+ prefix={prefix}
293
+ suffix={suffix}
294
+ className="bg-background"
295
+ />
296
+ );
297
+ }
298
+
299
+ /**
300
+ * Currency input component
301
+ */
302
+ function CurrencyInput({
303
+ definition,
304
+ value,
305
+ onChange,
306
+ readOnly,
307
+ }: Readonly<{
308
+ definition: ConfigurableOperationDefFragment['args'][number];
309
+ value: string;
310
+ onChange: (value: string) => void;
311
+ readOnly?: boolean;
312
+ }>) {
313
+ const numericValue = value ? parseInt(value, 10) : '';
314
+
315
+ return (
316
+ <AffixedInput
317
+ type="number"
318
+ value={numericValue}
319
+ onChange={e => {
320
+ const val = e.target.valueAsNumber;
321
+ onChange(isNaN(val) ? '0' : val.toString());
322
+ }}
323
+ disabled={readOnly}
324
+ min={0}
325
+ step={1}
326
+ prefix="$"
327
+ className="bg-background"
328
+ />
329
+ );
330
+ }
331
+
332
+ /**
333
+ * Select input component with options
334
+ */
335
+ function SelectInput({
336
+ definition,
337
+ value,
338
+ onChange,
339
+ readOnly,
340
+ }: Readonly<{
341
+ definition: ConfigurableOperationDefFragment['args'][number];
342
+ value: string;
343
+ onChange: (value: string) => void;
344
+ readOnly?: boolean;
345
+ }>) {
346
+ const ui = definition.ui as any;
347
+ const options = ui?.options || [];
348
+
349
+ return (
350
+ <Select value={value} onValueChange={onChange} disabled={readOnly}>
351
+ <SelectTrigger className="bg-background mb-0">
352
+ <SelectValue placeholder="Select an option..." />
353
+ </SelectTrigger>
354
+ <SelectContent>
355
+ {options.map((option: any) => (
356
+ <SelectItem key={option.value} value={option.value}>
357
+ {typeof option.label === 'string'
358
+ ? option.label
359
+ : option.label?.[0]?.value || option.value}
360
+ </SelectItem>
361
+ ))}
362
+ </SelectContent>
363
+ </Select>
364
+ );
365
+ }
366
+
367
+ /**
368
+ * Textarea input component
369
+ */
370
+ function TextareaInput({
371
+ definition,
372
+ value,
373
+ onChange,
374
+ readOnly,
375
+ }: Readonly<{
376
+ definition: ConfigurableOperationDefFragment['args'][number];
377
+ value: string;
378
+ onChange: (value: string) => void;
379
+ readOnly?: boolean;
380
+ }>) {
381
+ const ui = definition.ui as any;
382
+ const spellcheck = ui?.spellcheck ?? true;
383
+
384
+ return (
385
+ <Textarea
386
+ value={value || ''}
387
+ onChange={e => onChange(e.target.value)}
388
+ disabled={readOnly}
389
+ spellCheck={spellcheck}
390
+ placeholder="Enter text..."
391
+ rows={4}
392
+ className="bg-background"
393
+ />
394
+ );
395
+ }
@@ -1,8 +1,9 @@
1
1
  import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
2
2
  import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
3
- import { Trash } from 'lucide-react';
3
+ import { X } from 'lucide-react';
4
4
  import { useForm } from 'react-hook-form';
5
5
  import { Button } from '../ui/button.js';
6
+ import { Card, CardContent, CardHeader } from '../ui/card.js';
6
7
  import { Form, FormControl, FormField, FormItem, FormLabel } from '../ui/form.js';
7
8
  import { ConfigurableOperationArgInput } from './configurable-operation-arg-input.js';
8
9
 
@@ -26,7 +27,7 @@ export function ConfigurableOperationInput({
26
27
  value,
27
28
  onChange,
28
29
  onRemove,
29
- }: ConfigurableOperationInputProps) {
30
+ }: Readonly<ConfigurableOperationInputProps>) {
30
31
  const form = useForm({
31
32
  defaultValues: {
32
33
  ...value,
@@ -49,46 +50,85 @@ export function ConfigurableOperationInput({
49
50
  };
50
51
 
51
52
  return (
52
- <Form {...form}>
53
- <div className="space-y-4">
54
- <div className="flex flex-row justify-between">
55
- {!hideDescription && (
56
- <div className="font-medium">
57
- {' '}
58
- {interpolateDescription(operationDefinition, value.arguments)}
53
+ <div>
54
+ <Card className="bg-muted/50 shadow-none">
55
+ <CardHeader className="pb-3">
56
+ <div className="flex items-start justify-between">
57
+ <div className="flex-1 min-w-0">
58
+ {!hideDescription && (
59
+ <div className="font-medium text-sm text-foreground leading-relaxed">
60
+ {interpolateDescription(operationDefinition, value.arguments)}
61
+ </div>
62
+ )}
63
+
64
+ {operationDefinition.code && (
65
+ <div className="text-xs text-muted-foreground mt-1 font-mono">
66
+ {operationDefinition.code}
67
+ </div>
68
+ )}
59
69
  </div>
60
- )}
61
- {removable !== false && (
62
- <Button variant="outline" size="icon" onClick={onRemove}>
63
- <Trash />
64
- </Button>
65
- )}
66
- </div>
67
- <div className="grid grid-cols-2 gap-4">
68
- {operationDefinition.args.map(arg => {
69
- const argValue = value.arguments.find(a => a.name === arg.name)?.value || '';
70
- return (
71
- <FormField
72
- key={arg.name}
73
- name={`args.${arg.name}`}
74
- render={() => (
75
- <FormItem>
76
- <FormLabel>{arg.label || arg.name}</FormLabel>
77
- <FormControl>
78
- <ConfigurableOperationArgInput
79
- definition={arg}
80
- value={argValue}
81
- onChange={value => handleInputChange(arg.name, value)}
82
- />
83
- </FormControl>
84
- </FormItem>
85
- )}
86
- />
87
- );
88
- })}
89
- </div>
90
- </div>
91
- </Form>
70
+
71
+ {removable !== false && (
72
+ <Button
73
+ variant="ghost"
74
+ size="sm"
75
+ onClick={onRemove}
76
+ className="h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive"
77
+ disabled={readonly}
78
+ >
79
+ <X className="h-3.5 w-3.5" />
80
+ </Button>
81
+ )}
82
+ </div>
83
+ </CardHeader>
84
+
85
+ {operationDefinition.args && operationDefinition.args.length > 0 && (
86
+ <CardContent className="pt-0">
87
+ <Form {...form}>
88
+ <div className="space-y-4">
89
+ <div
90
+ className={`grid gap-4 ${operationDefinition.args.length === 1 ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}`}
91
+ >
92
+ {operationDefinition.args
93
+ .filter(
94
+ arg =>
95
+ arg.ui?.component !== 'combination-mode-form-input',
96
+ )
97
+ .map(arg => {
98
+ const argValue =
99
+ value.arguments.find(a => a.name === arg.name)?.value || '';
100
+ return (
101
+ <FormField
102
+ key={arg.name}
103
+ name={`args.${arg.name}`}
104
+ render={() => (
105
+ <FormItem className="space-y-2">
106
+ <FormLabel className="text-sm font-medium text-foreground">
107
+ {arg.label || arg.name}
108
+ </FormLabel>
109
+ <FormControl>
110
+ <ConfigurableOperationArgInput
111
+ definition={arg}
112
+ value={argValue}
113
+ onChange={value =>
114
+ handleInputChange(arg.name, value)
115
+ }
116
+ readOnly={readonly}
117
+ position={position}
118
+ />
119
+ </FormControl>
120
+ </FormItem>
121
+ )}
122
+ />
123
+ );
124
+ })}
125
+ </div>
126
+ </div>
127
+ </Form>
128
+ </CardContent>
129
+ )}
130
+ </Card>
131
+ </div>
92
132
  );
93
133
  }
94
134