@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,3 +1,8 @@
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
+ import { StructFormInput } from '@/vdb/components/data-input/struct-form-input.js';
1
6
  import {
2
7
  FormControl,
3
8
  FormDescription,
@@ -13,6 +18,7 @@ import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
13
18
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
14
19
  import { useLingui } from '@/vdb/lib/trans.js';
15
20
  import { customFieldConfigFragment } from '@/vdb/providers/server-config.js';
21
+ import { StringCustomFieldConfig } from '@vendure/common/lib/generated-types';
16
22
  import { CustomFieldType } from '@vendure/common/lib/shared-types';
17
23
  import { ResultOf } from 'gql.tada';
18
24
  import React, { useMemo } from 'react';
@@ -40,13 +46,15 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Readon
40
46
 
41
47
  const customFields = useCustomFieldConfig(entityType);
42
48
 
49
+ const getCustomFieldBaseName = (fieldDef: CustomFieldConfig) => {
50
+ if (fieldDef.type !== 'relation') {
51
+ return fieldDef.name;
52
+ }
53
+ return fieldDef.list ? fieldDef.name + 'Ids' : fieldDef.name + 'Id';
54
+ };
55
+
43
56
  const getFieldName = (fieldDef: CustomFieldConfig) => {
44
- const name =
45
- fieldDef.type === 'relation'
46
- ? fieldDef.list
47
- ? fieldDef.name + 'Ids'
48
- : fieldDef.name + 'Id'
49
- : fieldDef.name;
57
+ const name = getCustomFieldBaseName(fieldDef);
50
58
  return formPathPrefix ? `${formPathPrefix}.customFields.${name}` : `customFields.${name}`;
51
59
  };
52
60
 
@@ -84,7 +92,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Readon
84
92
  if (!shouldShowTabs) {
85
93
  // Single tab view - use the original grid layout
86
94
  return (
87
- <div className="grid grid-cols-2 gap-4">
95
+ <div className="grid @md:grid-cols-2 gap-6">
88
96
  {customFields?.map(fieldDef => (
89
97
  <CustomFieldItem
90
98
  key={fieldDef.name}
@@ -110,7 +118,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Readon
110
118
  </TabsList>
111
119
  {groupedFields.map(group => (
112
120
  <TabsContent key={group.tabName} value={group.tabName} className="mt-4">
113
- <div className="grid grid-cols-2 gap-4">
121
+ <div className="grid @md:grid-cols-2 gap-6">
114
122
  {group.customFields.map(fieldDef => (
115
123
  <CustomFieldItem
116
124
  key={fieldDef.name}
@@ -136,11 +144,12 @@ interface CustomFieldItemProps {
136
144
  ) => string | undefined;
137
145
  }
138
146
 
139
- function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: CustomFieldItemProps) {
140
- const hasCustomFormComponent = fieldDef.ui && fieldDef.ui.component;
147
+ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Readonly<CustomFieldItemProps>) {
148
+ const hasCustomFormComponent = fieldDef.ui?.component;
141
149
  const isLocaleField = fieldDef.type === 'localeString' || fieldDef.type === 'localeText';
142
150
  const shouldBeFullWidth = fieldDef.ui?.fullWidth === true;
143
151
  const containerClassName = shouldBeFullWidth ? 'col-span-2' : '';
152
+ const isReadonly = fieldDef.readonly ?? false;
144
153
 
145
154
  // For locale fields, always use TranslatableFormField regardless of custom components
146
155
  if (isLocaleField) {
@@ -207,6 +216,71 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Custo
207
216
  );
208
217
  }
209
218
 
219
+ // For struct fields, use the special struct component
220
+ if (fieldDef.type === 'struct') {
221
+ const isList = fieldDef.list ?? false;
222
+
223
+ // Handle struct lists - entire struct objects in a list
224
+ if (isList) {
225
+ return (
226
+ <div className={containerClassName}>
227
+ <FormField
228
+ control={control}
229
+ name={fieldName}
230
+ render={({ field }) => (
231
+ <FormItem>
232
+ <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
233
+ <FormControl>
234
+ <CustomFieldListInput
235
+ field={field}
236
+ disabled={isReadonly}
237
+ renderInput={(index, inputField) => (
238
+ <StructFormInput
239
+ field={inputField}
240
+ fieldDef={fieldDef as any}
241
+ control={control}
242
+ getTranslation={getTranslation}
243
+ />
244
+ )}
245
+ defaultValue={{}} // Empty struct object as default
246
+ isFullWidth={true} // Structs should always be full-width
247
+ />
248
+ </FormControl>
249
+ <FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
250
+ <FormMessage />
251
+ </FormItem>
252
+ )}
253
+ />
254
+ </div>
255
+ );
256
+ }
257
+
258
+ // Handle single struct fields
259
+ return (
260
+ <div className={containerClassName}>
261
+ <FormField
262
+ control={control}
263
+ name={fieldName}
264
+ render={({ field }) => (
265
+ <FormItem>
266
+ <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
267
+ <FormControl>
268
+ <StructFormInput
269
+ field={field}
270
+ fieldDef={fieldDef as any}
271
+ control={control}
272
+ getTranslation={getTranslation}
273
+ />
274
+ </FormControl>
275
+ <FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
276
+ <FormMessage />
277
+ </FormItem>
278
+ )}
279
+ />
280
+ </div>
281
+ );
282
+ }
283
+
210
284
  // For regular fields without custom components
211
285
  return (
212
286
  <div className={containerClassName}>
@@ -236,7 +310,12 @@ interface CustomFieldFormItemProps {
236
310
  children: React.ReactNode;
237
311
  }
238
312
 
239
- function CustomFieldFormItem({ fieldDef, getTranslation, fieldName, children }: CustomFieldFormItemProps) {
313
+ function CustomFieldFormItem({
314
+ fieldDef,
315
+ getTranslation,
316
+ fieldName,
317
+ children,
318
+ }: Readonly<CustomFieldFormItemProps>) {
240
319
  return (
241
320
  <FormItem>
242
321
  <FormLabel>{getTranslation(fieldDef.label) ?? fieldName}</FormLabel>
@@ -250,40 +329,132 @@ function CustomFieldFormItem({ fieldDef, getTranslation, fieldName, children }:
250
329
  function FormInputForType({
251
330
  fieldDef,
252
331
  field,
253
- }: {
332
+ }: Readonly<{
254
333
  fieldDef: CustomFieldConfig;
255
334
  field: ControllerRenderProps<any, any>;
256
- }) {
335
+ }>) {
257
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;
258
349
 
259
- switch (fieldDef.type as CustomFieldType) {
260
- case 'string':
261
- return <Input {...field} disabled={isReadonly} />;
262
- case 'float':
263
- case 'int':
264
- return (
265
- <Input
266
- type="number"
267
- {...field}
268
- disabled={isReadonly}
269
- onChange={e => field.onChange(e.target.valueAsNumber)}
270
- />
271
- );
272
- case 'boolean':
273
- return <Switch checked={field.value} onCheckedChange={field.onChange} disabled={isReadonly} />;
274
- case 'relation':
275
- if (fieldDef.list) {
276
350
  return (
277
351
  <Input
278
- {...field}
279
- onChange={e => field.onChange(e.target.value.split(','))}
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}
280
380
  disabled={isReadonly}
281
381
  />
282
382
  );
283
- } else {
284
- return <Input {...field} disabled={isReadonly} />;
285
383
  }
286
- default:
287
- return <Input {...field} disabled={isReadonly} />;
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
288
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);
289
460
  }
@@ -71,7 +71,7 @@ export function MultiSelect<T extends boolean>(props: MultiSelectProps<T>) {
71
71
  className={cn(
72
72
  'w-full justify-between',
73
73
  'min-h-[2.5rem] h-auto',
74
- 'flex flex-wrap gap-1 p-1',
74
+ 'flex flex-wrap gap-1 p-1 shadow-xs',
75
75
  className,
76
76
  )}
77
77
  >
@@ -3,12 +3,12 @@ import { Slot } from '@radix-ui/react-slot';
3
3
  import * as React from 'react';
4
4
  import {
5
5
  Controller,
6
- FormProvider,
7
- useFormContext,
8
- useFormState,
9
6
  type ControllerProps,
10
7
  type FieldPath,
11
8
  type FieldValues,
9
+ FormProvider,
10
+ useFormContext,
11
+ useFormState,
12
12
  } from 'react-hook-form';
13
13
 
14
14
  import { Label } from '@/vdb/components/ui/label.js';
@@ -112,7 +112,7 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
112
112
  <p
113
113
  data-slot="form-description"
114
114
  id={formDescriptionId}
115
- className={cn('text-muted-foreground text-sm', className)}
115
+ className={cn('text-muted-foreground text-xs', className)}
116
116
  {...props}
117
117
  />
118
118
  );
@@ -1,6 +1,8 @@
1
+ import { CombinationModeInput } from '@/vdb/components/data-input/combination-mode-input.js';
1
2
  import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
2
3
  import { FacetValueInput } from '@/vdb/components/data-input/facet-value-input.js';
3
4
  import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
5
+ import { ProductMultiInput } from '@/vdb/components/data-input/product-multi-selector.js';
4
6
  import { Checkbox } from '@/vdb/components/ui/checkbox.js';
5
7
  import { Input } from '@/vdb/components/ui/input.js';
6
8
  import { DataInputComponent } from '../component-registry/component-registry.js';
@@ -31,6 +33,8 @@ inputComponents.set('vendure:numberInput', NumberInput);
31
33
  inputComponents.set('vendure:dateTimeInput', DateTimeInput);
32
34
  inputComponents.set('vendure:checkboxInput', CheckboxInput);
33
35
  inputComponents.set('vendure:facetValueInput', FacetValueInput);
36
+ inputComponents.set('vendure:combinationModeInput', CombinationModeInput);
37
+ inputComponents.set('vendure:productMultiInput', ProductMultiInput);
34
38
 
35
39
  export function getInputComponent(id: string): DataInputComponent | undefined {
36
40
  return globalRegistry.get('inputComponents').get(id);
@@ -54,7 +58,7 @@ export function addInputComponent({
54
58
  pageId: string;
55
59
  blockId: string;
56
60
  field: string;
57
- component: React.ComponentType<{ value: any; onChange: (value: any) => void; [key: string]: any }>;
61
+ component: DataInputComponent;
58
62
  }) {
59
63
  const inputComponents = globalRegistry.get('inputComponents');
60
64