@vendure/dashboard 3.4.1-master-202508210231 → 3.4.1

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 (58) hide show
  1. package/package.json +152 -152
  2. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +2 -1
  3. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +1 -0
  4. package/src/lib/components/data-input/affixed-input.tsx +19 -5
  5. package/src/lib/components/data-input/boolean-input.tsx +9 -0
  6. package/src/lib/components/data-input/checkbox-input.tsx +8 -0
  7. package/src/lib/components/data-input/combination-mode-input.tsx +11 -2
  8. package/src/lib/components/data-input/configurable-operation-list-input.tsx +26 -401
  9. package/src/lib/components/data-input/custom-field-list-input.tsx +18 -25
  10. package/src/lib/components/data-input/customer-group-input.tsx +7 -11
  11. package/src/lib/components/data-input/datetime-input.tsx +9 -13
  12. package/src/lib/components/data-input/default-relation-input.tsx +29 -9
  13. package/src/lib/components/data-input/facet-value-input.tsx +15 -13
  14. package/src/lib/components/data-input/index.ts +2 -2
  15. package/src/lib/components/data-input/money-input.tsx +27 -20
  16. package/src/lib/components/data-input/number-input.tsx +48 -0
  17. package/src/lib/components/data-input/password-input.tsx +16 -0
  18. package/src/lib/components/data-input/{product-multi-selector.tsx → product-multi-selector-input.tsx} +8 -15
  19. package/src/lib/components/data-input/relation-input.tsx +7 -6
  20. package/src/lib/components/data-input/rich-text-input.tsx +10 -13
  21. package/src/lib/components/data-input/select-with-options.tsx +29 -17
  22. package/src/lib/components/data-input/struct-form-input.tsx +54 -59
  23. package/src/lib/components/data-input/text-input.tsx +9 -0
  24. package/src/lib/components/data-input/textarea-input.tsx +16 -0
  25. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +3 -0
  26. package/src/lib/components/data-table/use-generated-columns.tsx +16 -5
  27. package/src/lib/components/shared/configurable-operation-arg-input.tsx +3 -10
  28. package/src/lib/components/shared/configurable-operation-input.tsx +1 -6
  29. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +8 -5
  30. package/src/lib/components/shared/configurable-operation-selector.tsx +5 -5
  31. package/src/lib/components/shared/custom-fields-form.tsx +20 -49
  32. package/src/lib/components/shared/multi-select.tsx +1 -1
  33. package/src/lib/framework/component-registry/component-registry.tsx +9 -32
  34. package/src/lib/framework/component-registry/display-component.tsx +28 -0
  35. package/src/lib/framework/extension-api/display-component-extensions.tsx +0 -14
  36. package/src/lib/framework/extension-api/input-component-extensions.tsx +52 -34
  37. package/src/lib/framework/extension-api/logic/data-table.ts +4 -27
  38. package/src/lib/framework/extension-api/logic/form-components.ts +3 -2
  39. package/src/lib/framework/extension-api/types/detail-forms.ts +2 -38
  40. package/src/lib/framework/extension-api/types/form-components.ts +2 -4
  41. package/src/lib/framework/form-engine/custom-form-component-extensions.ts +0 -23
  42. package/src/lib/framework/form-engine/custom-form-component.tsx +8 -25
  43. package/src/lib/framework/form-engine/default-input-for-type.tsx +35 -0
  44. package/src/lib/framework/form-engine/form-control-adapter.tsx +192 -0
  45. package/src/lib/framework/form-engine/form-engine-types.ts +163 -0
  46. package/src/lib/framework/form-engine/form-schema-tools.ts +55 -71
  47. package/src/lib/framework/form-engine/overridden-form-component.tsx +2 -2
  48. package/src/lib/framework/form-engine/utils.ts +223 -0
  49. package/src/lib/{components/shared → framework/form-engine}/value-transformers.ts +9 -9
  50. package/src/lib/framework/registry/registry-types.ts +3 -5
  51. package/src/lib/graphql/graphql-env.d.ts +11 -7
  52. package/src/lib/index.ts +28 -1
  53. package/src/lib/providers/server-config.tsx +1 -0
  54. package/src/lib/components/shared/direct-form-component-map.tsx +0 -393
  55. package/src/lib/components/shared/universal-field-definition.ts +0 -118
  56. package/src/lib/components/shared/universal-form-input.tsx +0 -175
  57. package/src/lib/components/shared/universal-input-components.tsx +0 -291
  58. package/src/lib/framework/component-registry/dynamic-component.tsx +0 -58
@@ -5,6 +5,9 @@ import { ControllerRenderProps } from 'react-hook-form';
5
5
  import { MultiRelationInput, SingleRelationInput } from './relation-input.js';
6
6
  import { createRelationSelectorConfig } from './relation-selector.js';
7
7
 
8
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
9
+ import { isRelationCustomFieldConfig } from '@/vdb/framework/form-engine/utils.js';
10
+
8
11
  interface PlaceholderIconProps {
9
12
  letter: string;
10
13
  className?: string;
@@ -551,8 +554,19 @@ interface DefaultRelationInputProps {
551
554
  disabled?: boolean;
552
555
  }
553
556
 
554
- export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<DefaultRelationInputProps>) {
557
+ export function DefaultRelationInput({
558
+ fieldDef,
559
+ value,
560
+ onChange,
561
+ onBlur,
562
+ name,
563
+ ref,
564
+ disabled,
565
+ }: Readonly<DashboardFormComponentProps>) {
555
566
  const { i18n } = useLingui();
567
+ if (!fieldDef || !isRelationCustomFieldConfig(fieldDef)) {
568
+ return null;
569
+ }
556
570
  const entityName = fieldDef.entity;
557
571
  const ENTITY_CONFIGS = createEntityConfigs(i18n);
558
572
  const config = ENTITY_CONFIGS[entityName as keyof typeof ENTITY_CONFIGS];
@@ -562,10 +576,10 @@ export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<Def
562
576
  console.warn(`No relation selector config found for entity: ${entityName}`);
563
577
  return (
564
578
  <input
565
- value={field.value ?? ''}
566
- onChange={e => field.onChange(e.target.value)}
567
- onBlur={field.onBlur}
568
- name={field.name}
579
+ value={value ?? ''}
580
+ onChange={e => onChange(e.target.value)}
581
+ onBlur={onBlur}
582
+ name={name}
569
583
  disabled={disabled}
570
584
  placeholder={`Enter ${entityName} ID`}
571
585
  className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
@@ -578,8 +592,11 @@ export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<Def
578
592
  if (isList) {
579
593
  return (
580
594
  <MultiRelationInput
581
- value={field.value ?? []}
582
- onChange={field.onChange}
595
+ onBlur={onBlur}
596
+ name={name}
597
+ ref={ref}
598
+ value={value ?? []}
599
+ onChange={onChange}
583
600
  config={config}
584
601
  disabled={disabled}
585
602
  selectorLabel={<Trans>Select {entityName.toLowerCase()}s</Trans>}
@@ -588,8 +605,11 @@ export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<Def
588
605
  } else {
589
606
  return (
590
607
  <SingleRelationInput
591
- value={field.value ?? ''}
592
- onChange={field.onChange}
608
+ onBlur={onBlur}
609
+ name={name}
610
+ ref={ref}
611
+ value={value ?? ''}
612
+ onChange={onChange}
593
613
  config={config}
594
614
  disabled={disabled}
595
615
  selectorLabel={<Trans>Select {entityName.toLowerCase()}</Trans>}
@@ -1,3 +1,4 @@
1
+ import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
1
2
  import { api } from '@/vdb/graphql/api.js';
2
3
  import { graphql } from '@/vdb/graphql/graphql.js';
3
4
  import { useQuery } from '@tanstack/react-query';
@@ -21,14 +22,8 @@ const facetValuesDocument = graphql(`
21
22
  }
22
23
  `);
23
24
 
24
- export interface FacetValueInputProps {
25
- value: string;
26
- onChange: (value: string) => void;
27
- readOnly?: boolean;
28
- }
29
-
30
- export function FacetValueInput(props: FacetValueInputProps) {
31
- const ids = decodeIds(props.value);
25
+ export const FacetValueInput: DashboardFormComponent = ({ value, onChange, disabled }) => {
26
+ const ids = decodeIds(value);
32
27
  const { data } = useQuery({
33
28
  queryKey: ['facetValues', ids],
34
29
  queryFn: () =>
@@ -43,12 +38,12 @@ export function FacetValueInput(props: FacetValueInputProps) {
43
38
 
44
39
  const onValueSelectHandler = (value: FacetValue) => {
45
40
  const newIds = new Set([...ids, value.id]);
46
- props.onChange(JSON.stringify(Array.from(newIds)));
41
+ onChange(JSON.stringify(Array.from(newIds)));
47
42
  };
48
43
 
49
44
  const onValueRemoveHandler = (id: string) => {
50
45
  const newIds = new Set(ids.filter(existingId => existingId !== id));
51
- props.onChange(JSON.stringify(Array.from(newIds)));
46
+ onChange(JSON.stringify(Array.from(newIds)));
52
47
  };
53
48
 
54
49
  return (
@@ -62,12 +57,19 @@ export function FacetValueInput(props: FacetValueInputProps) {
62
57
  />
63
58
  ))}
64
59
  </div>
65
- <FacetValueSelector onValueSelect={onValueSelectHandler} disabled={props.readOnly} />
60
+ <FacetValueSelector onValueSelect={onValueSelectHandler} disabled={disabled} />
66
61
  </div>
67
62
  );
68
- }
63
+ };
69
64
 
70
- function decodeIds(idsString: string): string[] {
65
+ FacetValueInput.metadata = {
66
+ isListInput: true,
67
+ };
68
+
69
+ function decodeIds(idsString: string | string[]): string[] {
70
+ if (Array.isArray(idsString)) {
71
+ return idsString;
72
+ }
71
73
  try {
72
74
  return JSON.parse(idsString);
73
75
  } catch (error) {
@@ -9,8 +9,8 @@ export * from './select-with-options.js';
9
9
 
10
10
  // Enhanced configurable operation input components
11
11
  export * from './configurable-operation-list-input.js';
12
- export * from './customer-group-selector-input.js';
13
- export * from './product-selector-input.js';
12
+ export * from './customer-group-input.js';
13
+ export * from './product-multi-selector-input.js';
14
14
 
15
15
  // Relation selector components
16
16
  export * from './relation-input.js';
@@ -1,11 +1,21 @@
1
- import { DataInputComponentProps } from '@/vdb/framework/component-registry/component-registry.js';
2
1
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
2
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
4
3
  import { useEffect, useMemo, useState } from 'react';
5
4
  import { AffixedInput } from './affixed-input.js';
6
5
 
7
- // Original component
8
- function MoneyInputInternal({ value, currency, onChange }: DataInputComponentProps) {
6
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
7
+ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
8
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
9
+
10
+ export interface MoneyInputProps extends DashboardFormComponentProps {
11
+ currency?: string;
12
+ }
13
+
14
+ export function MoneyInput(props: Readonly<MoneyInputProps>) {
15
+ const { value, onChange, currency, ...rest } = props;
16
+ const { activeChannel } = useChannel();
17
+ const activeCurrency = currency ?? activeChannel?.defaultCurrencyCode;
18
+ const readOnly = isReadonlyField(props.fieldDef);
9
19
  const {
10
20
  settings: { displayLanguage, displayLocale },
11
21
  } = useUserSettings();
@@ -19,39 +29,43 @@ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentPro
19
29
 
20
30
  // Determine if the currency symbol should be a prefix based on locale
21
31
  const shouldPrefix = useMemo(() => {
22
- if (!currency) return false;
32
+ if (!activeCurrency) {
33
+ return false;
34
+ }
23
35
  const locale = displayLocale || displayLanguage.replace(/_/g, '-');
24
36
  const parts = new Intl.NumberFormat(locale, {
25
37
  style: 'currency',
26
- currency,
38
+ currency: activeCurrency,
27
39
  currencyDisplay: 'symbol',
28
40
  }).formatToParts();
29
41
  const NaNString = parts.find(p => p.type === 'nan')?.value ?? 'NaN';
30
42
  const localised = new Intl.NumberFormat(locale, {
31
43
  style: 'currency',
32
- currency,
44
+ currency: activeCurrency,
33
45
  currencyDisplay: 'symbol',
34
46
  }).format(undefined as any);
35
47
  return localised.indexOf(NaNString) > 0;
36
- }, [currency, displayLocale, displayLanguage]);
48
+ }, [activeCurrency, displayLocale, displayLanguage]);
37
49
 
38
50
  // Get the currency symbol
39
51
  const currencySymbol = useMemo(() => {
40
- if (!currency) return '';
52
+ if (!activeCurrency) return '';
41
53
  const locale = displayLocale || displayLanguage.replace(/_/g, '-');
42
54
  const parts = new Intl.NumberFormat(locale, {
43
55
  style: 'currency',
44
- currency,
56
+ currency: activeCurrency,
45
57
  currencyDisplay: 'symbol',
46
58
  }).formatToParts();
47
- return parts.find(p => p.type === 'currency')?.value ?? currency;
48
- }, [currency, displayLocale, displayLanguage]);
59
+ return parts.find(p => p.type === 'currency')?.value ?? activeCurrency;
60
+ }, [activeCurrency, displayLocale, displayLanguage]);
49
61
 
50
62
  return (
51
63
  <AffixedInput
52
64
  type="text"
53
65
  className="bg-background"
54
66
  value={displayValue}
67
+ disabled={readOnly}
68
+ {...rest}
55
69
  onChange={e => {
56
70
  const inputValue = e.target.value;
57
71
  // Allow empty input
@@ -77,8 +91,8 @@ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentPro
77
91
  }
78
92
  }
79
93
  }}
80
- onBlur={e => {
81
- const inputValue = e.target.value;
94
+ onBlur={() => {
95
+ const inputValue = displayValue;
82
96
  if (inputValue === '') {
83
97
  onChange(0);
84
98
  setDisplayValue('0');
@@ -97,10 +111,3 @@ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentPro
97
111
  />
98
112
  );
99
113
  }
100
-
101
- // Wrapper that makes it compatible with DataInputComponent
102
- export function MoneyInput(props: { value: any; onChange: (value: any) => void; [key: string]: any }) {
103
- const { value, onChange, ...rest } = props;
104
- const currency = rest.currency || 'USD'; // Default currency if none provided
105
- return <MoneyInputInternal value={value} currency={currency} onChange={onChange} />;
106
- }
@@ -0,0 +1,48 @@
1
+ import { AffixedInput } from '@/vdb/components/data-input/affixed-input.js';
2
+ import { Input } from '@/vdb/components/ui/input.js';
3
+
4
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
5
+ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
6
+
7
+ export function NumberInput({ fieldDef, onChange, ...fieldProps }: Readonly<DashboardFormComponentProps>) {
8
+ const readOnly = fieldProps.disabled || isReadonlyField(fieldDef);
9
+ const isFloat = fieldDef ? fieldDef.type === 'float' : false;
10
+ const min = fieldDef?.ui?.min;
11
+ const max = fieldDef?.ui?.max;
12
+ const step = fieldDef?.ui?.step || (isFloat ? 0.01 : 1);
13
+ const prefix = fieldDef?.ui?.prefix;
14
+ const suffix = fieldDef?.ui?.suffix;
15
+ const shouldUseAffixedInput = prefix || suffix;
16
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
17
+ if (readOnly) return;
18
+ onChange(e.target.valueAsNumber);
19
+ };
20
+ if (shouldUseAffixedInput) {
21
+ return (
22
+ <AffixedInput
23
+ {...fieldProps}
24
+ type="number"
25
+ onChange={handleChange}
26
+ min={min}
27
+ max={max}
28
+ step={step}
29
+ prefix={prefix}
30
+ suffix={suffix}
31
+ className="bg-background"
32
+ disabled={readOnly}
33
+ />
34
+ );
35
+ }
36
+
37
+ return (
38
+ <Input
39
+ type="number"
40
+ onChange={handleChange}
41
+ {...fieldProps}
42
+ min={min}
43
+ max={max}
44
+ step={step}
45
+ disabled={readOnly}
46
+ />
47
+ );
48
+ }
@@ -0,0 +1,16 @@
1
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
2
+ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
3
+ import { Input } from '../ui/input.js';
4
+
5
+ export function PasswordInput(props: Readonly<DashboardFormComponentProps>) {
6
+ const readOnly = props.disabled || isReadonlyField(props.fieldDef);
7
+ return (
8
+ <Input
9
+ type="password"
10
+ ref={props.ref}
11
+ value={props.value}
12
+ onChange={e => props.onChange(e.target.value)}
13
+ disabled={readOnly}
14
+ />
15
+ );
16
+ }
@@ -10,7 +10,7 @@ import {
10
10
  DialogTitle,
11
11
  } from '@/vdb/components/ui/dialog.js';
12
12
  import { Input } from '@/vdb/components/ui/input.js';
13
- import { DataInputComponent } from '@/vdb/framework/component-registry/component-registry.js';
13
+ import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
14
14
  import { api } from '@/vdb/graphql/api.js';
15
15
  import { graphql } from '@/vdb/graphql/graphql.js';
16
16
  import { Trans } from '@/vdb/lib/trans.js';
@@ -372,21 +372,12 @@ function ProductMultiSelectorDialog({
372
372
  );
373
373
  }
374
374
 
375
- export const ProductMultiInput: DataInputComponent = ({ value, onChange, ...props }) => {
375
+ export const ProductMultiInput: DashboardFormComponent = ({ value, onChange, ...props }) => {
376
376
  const [open, setOpen] = useState(false);
377
-
378
377
  // Parse the configuration from the field definition
379
- const mode = (props as any)?.selectionMode === 'variant' ? 'variant' : 'product';
380
-
378
+ const mode = props.fieldDef?.ui?.selectionMode === 'variant' ? 'variant' : 'product';
381
379
  // Parse the current value (JSON array of IDs)
382
- const selectedIds = useMemo(() => {
383
- if (!value || typeof value !== 'string') return [];
384
- try {
385
- return JSON.parse(value);
386
- } catch {
387
- return [];
388
- }
389
- }, [value]);
380
+ const selectedIds = value;
390
381
 
391
382
  const handleSelectionChange = useCallback(
392
383
  (newSelectedIds: string[]) => {
@@ -394,11 +385,9 @@ export const ProductMultiInput: DataInputComponent = ({ value, onChange, ...prop
394
385
  },
395
386
  [onChange],
396
387
  );
397
-
398
388
  const itemType = mode === 'product' ? 'products' : 'variants';
399
389
  const buttonText =
400
390
  selectedIds.length > 0 ? `Selected ${selectedIds.length} ${itemType}` : `Select ${itemType}`;
401
-
402
391
  return (
403
392
  <>
404
393
  <div className="space-y-2">
@@ -424,3 +413,7 @@ export const ProductMultiInput: DataInputComponent = ({ value, onChange, ...prop
424
413
  </>
425
414
  );
426
415
  };
416
+
417
+ ProductMultiInput.metadata = {
418
+ isListInput: true,
419
+ };
@@ -1,12 +1,11 @@
1
+ import { DashboardFormComponent, DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
1
2
  import { graphql } from '@/vdb/graphql/graphql.js';
2
3
  import { createRelationSelectorConfig, RelationSelector } from './relation-selector.js';
3
4
 
4
5
  /**
5
6
  * Single relation input component
6
7
  */
7
- export interface SingleRelationInputProps<T = any> {
8
- value: string;
9
- onChange: (value: string) => void;
8
+ export interface SingleRelationInputProps<T = any> extends DashboardFormComponentProps {
10
9
  config: Parameters<typeof createRelationSelectorConfig<T>>[0];
11
10
  disabled?: boolean;
12
11
  className?: string;
@@ -46,9 +45,7 @@ export function SingleRelationInput<T>({
46
45
  /**
47
46
  * Multi relation input component
48
47
  */
49
- export interface MultiRelationInputProps<T = any> {
50
- value: string[];
51
- onChange: (value: string[]) => void;
48
+ export interface MultiRelationInputProps<T = any> extends DashboardFormComponentProps {
52
49
  config: Parameters<typeof createRelationSelectorConfig<T>>[0];
53
50
  disabled?: boolean;
54
51
  className?: string;
@@ -80,6 +77,10 @@ export function MultiRelationInput<T>({
80
77
  );
81
78
  }
82
79
 
80
+ (MultiRelationInput as DashboardFormComponent).metadata = {
81
+ isListInput: true,
82
+ };
83
+
83
84
  // Example configurations for common entities
84
85
 
85
86
  /**
@@ -1,9 +1,11 @@
1
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
1
2
  import TextStyle from '@tiptap/extension-text-style';
2
3
  import { BubbleMenu, Editor, EditorContent, useEditor } from '@tiptap/react';
3
4
  import StarterKit from '@tiptap/starter-kit';
4
5
  import { BoldIcon, ItalicIcon, StrikethroughIcon } from 'lucide-react';
5
6
  import { useLayoutEffect, useRef } from 'react';
6
7
  import { Button } from '../ui/button.js';
8
+ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
7
9
 
8
10
  // define your extension array
9
11
  const extensions = [
@@ -20,13 +22,8 @@ const extensions = [
20
22
  }),
21
23
  ];
22
24
 
23
- export interface RichTextInputProps {
24
- value: string;
25
- disabled?: boolean;
26
- onChange: (value: string) => void;
27
- }
28
-
29
- export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextInputProps>) {
25
+ export function RichTextInput({ value, onChange, fieldDef }: Readonly<DashboardFormComponentProps>) {
26
+ const readOnly = isReadonlyField(fieldDef);
30
27
  const isInternalUpdate = useRef(false);
31
28
 
32
29
  const editor = useEditor({
@@ -35,16 +32,16 @@ export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextIn
35
32
  },
36
33
  extensions: extensions,
37
34
  content: value,
38
- editable: !disabled,
35
+ editable: !readOnly,
39
36
  onUpdate: ({ editor }) => {
40
- if (!disabled) {
37
+ if (!readOnly) {
41
38
  isInternalUpdate.current = true;
42
39
  onChange(editor.getHTML());
43
40
  }
44
41
  },
45
42
  editorProps: {
46
43
  attributes: {
47
- class: `border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[500px] overflow-y-auto ${disabled ? 'cursor-not-allowed opacity-50' : ''}`,
44
+ class: `border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[500px] overflow-y-auto ${readOnly ? 'cursor-not-allowed opacity-50' : ''}`,
48
45
  },
49
46
  },
50
47
  });
@@ -64,9 +61,9 @@ export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextIn
64
61
  // Update editor's editable state when disabled prop changes
65
62
  useLayoutEffect(() => {
66
63
  if (editor) {
67
- editor.setEditable(!disabled);
64
+ editor.setEditable(!readOnly);
68
65
  }
69
- }, [disabled, editor]);
66
+ }, [readOnly, editor]);
70
67
 
71
68
  if (!editor) {
72
69
  return null;
@@ -75,7 +72,7 @@ export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextIn
75
72
  return (
76
73
  <>
77
74
  <EditorContent editor={editor} />
78
- <CustomBubbleMenu editor={editor} disabled={disabled} />
75
+ <CustomBubbleMenu editor={editor} disabled={readOnly} />
79
76
  </>
80
77
  );
81
78
  }
@@ -1,15 +1,16 @@
1
1
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
2
+ import {
3
+ DashboardFormComponent,
4
+ DashboardFormComponentProps,
5
+ StringCustomFieldConfig,
6
+ } from '@/vdb/framework/form-engine/form-engine-types.js';
7
+ import { isReadonlyField, isStringFieldWithOptions } from '@/vdb/framework/form-engine/utils.js';
2
8
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
3
9
  import { Trans } from '@/vdb/lib/trans.js';
4
- import { StringFieldOption } from '@vendure/common/lib/generated-types';
5
10
  import React from 'react';
6
- import { ControllerRenderProps } from 'react-hook-form';
7
11
  import { MultiSelect } from '../shared/multi-select.js';
8
12
 
9
- export interface SelectWithOptionsProps {
10
- field: ControllerRenderProps<any, any>;
11
- options: StringFieldOption[];
12
- disabled?: boolean;
13
+ export interface SelectWithOptionsProps extends DashboardFormComponentProps {
13
14
  placeholder?: React.ReactNode;
14
15
  isListField?: boolean;
15
16
  }
@@ -22,12 +23,14 @@ export interface SelectWithOptionsProps {
22
23
  * @since 3.3.0
23
24
  */
24
25
  export function SelectWithOptions({
25
- field,
26
- options,
27
- disabled,
26
+ value,
27
+ onChange,
28
+ fieldDef,
28
29
  placeholder,
29
30
  isListField = false,
31
+ disabled,
30
32
  }: Readonly<SelectWithOptionsProps>) {
33
+ const readOnly = disabled || isReadonlyField(fieldDef);
31
34
  const {
32
35
  settings: { displayLanguage },
33
36
  } = useUserSettings();
@@ -37,6 +40,11 @@ export function SelectWithOptions({
37
40
  const translation = label.find(t => t.languageCode === displayLanguage);
38
41
  return translation?.value ?? label[0]?.value ?? '';
39
42
  };
43
+ if (!fieldDef || !isStringFieldWithOptions(fieldDef)) {
44
+ return null;
45
+ }
46
+ const options: NonNullable<StringCustomFieldConfig['options']> =
47
+ fieldDef.options ?? fieldDef.ui.options ?? [];
40
48
 
41
49
  // Convert options to MultiSelect format
42
50
  const multiSelectItems = options.map(option => ({
@@ -45,31 +53,31 @@ export function SelectWithOptions({
45
53
  }));
46
54
 
47
55
  // For list fields, use MultiSelect component
48
- if (isListField) {
56
+ if (isListField || fieldDef?.list === true) {
49
57
  return (
50
58
  <MultiSelect
51
59
  multiple={true}
52
- value={field.value || []}
53
- onChange={field.onChange}
60
+ value={value || []}
61
+ onChange={onChange}
54
62
  items={multiSelectItems}
55
63
  placeholder={placeholder ? String(placeholder) : 'Select options'}
56
- className={disabled ? 'opacity-50 pointer-events-none' : ''}
64
+ className={readOnly ? 'opacity-50 pointer-events-none' : ''}
57
65
  />
58
66
  );
59
67
  }
60
68
 
61
69
  // For single fields, use regular Select
62
- const currentValue = field.value ?? '';
70
+ const currentValue = value ?? '';
63
71
 
64
72
  const handleValueChange = (value: string) => {
65
73
  if (value) {
66
- field.onChange(value);
74
+ onChange(value);
67
75
  }
68
76
  };
69
77
 
70
78
  return (
71
- <Select value={currentValue ?? undefined} onValueChange={handleValueChange} disabled={disabled}>
72
- <SelectTrigger>
79
+ <Select value={currentValue ?? undefined} onValueChange={handleValueChange} disabled={readOnly}>
80
+ <SelectTrigger className="mb-0">
73
81
  <SelectValue placeholder={placeholder || <Trans>Select an option</Trans>} />
74
82
  </SelectTrigger>
75
83
  <SelectContent>
@@ -82,3 +90,7 @@ export function SelectWithOptions({
82
90
  </Select>
83
91
  );
84
92
  }
93
+
94
+ (SelectWithOptions as DashboardFormComponent).metadata = {
95
+ isListInput: 'dynamic',
96
+ };