Explorar o código

refactor(dashboard): Rework custom form input handling (#3735)

Michael Bromley hai 5 meses
pai
achega
46a12b7f72

+ 1 - 0
packages/dashboard/src/lib/components/data-input/money-input.tsx

@@ -50,6 +50,7 @@ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentPro
     return (
         <AffixedInput
             type="text"
+            className="bg-background"
             value={displayValue}
             onChange={e => {
                 const inputValue = e.target.value;

+ 21 - 7
packages/dashboard/src/lib/components/data-input/rich-text-input.tsx

@@ -22,10 +22,11 @@ const extensions = [
 
 export interface RichTextInputProps {
     value: string;
+    disabled?: boolean;
     onChange: (value: string) => void;
 }
 
-export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>) {
+export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextInputProps>) {
     const isInternalUpdate = useRef(false);
 
     const editor = useEditor({
@@ -34,13 +35,16 @@ export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>)
         },
         extensions: extensions,
         content: value,
+        editable: !disabled,
         onUpdate: ({ editor }) => {
-            isInternalUpdate.current = true;
-            onChange(editor.getHTML());
+            if (!disabled) {
+                isInternalUpdate.current = true;
+                onChange(editor.getHTML());
+            }
         },
         editorProps: {
             attributes: {
-                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',
+                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' : ''}`,
             },
         },
     });
@@ -57,6 +61,13 @@ export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>)
         isInternalUpdate.current = false;
     }, [value, editor]);
 
+    // Update editor's editable state when disabled prop changes
+    useLayoutEffect(() => {
+        if (editor) {
+            editor.setEditable(!disabled);
+        }
+    }, [disabled, editor]);
+
     if (!editor) {
         return null;
     }
@@ -64,13 +75,13 @@ export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>)
     return (
         <>
             <EditorContent editor={editor} />
-            <CustomBubbleMenu editor={editor} />
+            <CustomBubbleMenu editor={editor} disabled={disabled} />
         </>
     );
 }
 
-function CustomBubbleMenu({ editor }: { editor: Editor | null }) {
-    if (!editor) return null;
+function CustomBubbleMenu({ editor, disabled }: { editor: Editor | null; disabled?: boolean }) {
+    if (!editor || disabled) return null;
     return (
         <BubbleMenu editor={editor}>
             <div className="flex items-center gap-2 bg-background p-2 rounded-md border">
@@ -80,6 +91,7 @@ function CustomBubbleMenu({ editor }: { editor: Editor | null }) {
                     size="icon"
                     onClick={() => editor.chain().focus().toggleBold().run()}
                     className={editor.isActive('bold') ? 'bg-accent' : ''}
+                    disabled={disabled}
                 >
                     <BoldIcon className="w-4 h-4" />
                 </Button>
@@ -89,6 +101,7 @@ function CustomBubbleMenu({ editor }: { editor: Editor | null }) {
                     size="icon"
                     onClick={() => editor.chain().focus().toggleItalic().run()}
                     className={editor.isActive('italic') ? 'bg-accent' : ''}
+                    disabled={disabled}
                 >
                     <ItalicIcon className="w-4 h-4" />
                 </Button>
@@ -98,6 +111,7 @@ function CustomBubbleMenu({ editor }: { editor: Editor | null }) {
                     size="icon"
                     onClick={() => editor.chain().focus().toggleStrike().run()}
                     className={editor.isActive('strike') ? 'bg-accent' : ''}
+                    disabled={disabled}
                 >
                     <StrikethroughIcon className="w-4 h-4" />
                 </Button>

+ 13 - 372
packages/dashboard/src/lib/components/shared/configurable-operation-arg-input.tsx

@@ -1,16 +1,6 @@
-import { InputComponent } from '@/vdb/framework/component-registry/dynamic-component.js';
 import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
-import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types';
-import { ConfigArgType } from '@vendure/core';
-import { AffixedInput } from '../data-input/affixed-input.js';
-import { ConfigurableOperationListInput } from '../data-input/configurable-operation-list-input.js';
-import { DateTimeInput } from '../data-input/datetime-input.js';
-import { DefaultRelationInput } from '../data-input/default-relation-input.js';
-import { FacetValueInput } from '../data-input/facet-value-input.js';
-import { Input } from '../ui/input.js';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
-import { Switch } from '../ui/switch.js';
-import { Textarea } from '../ui/textarea.js';
+import { configArgToUniversal } from './universal-field-definition.js';
+import { UniversalFormInput } from './universal-form-input.js';
 
 export interface ConfigurableOperationArgInputProps {
     definition: ConfigurableOperationDefFragment['args'][number];
@@ -20,30 +10,6 @@ export interface ConfigurableOperationArgInputProps {
     position?: number;
 }
 
-/**
- * Maps Vendure UI component names to their corresponding Dashboard input component IDs
- */
-const UI_COMPONENT_MAP = {
-    'number-form-input': 'vendure:numberInput',
-    'currency-form-input': 'vendure:currencyInput',
-    'facet-value-form-input': 'facet-value-input',
-    'product-selector-form-input': 'vendure:productSelectorInput',
-    'customer-group-form-input': 'vendure:customerGroupInput',
-    'date-form-input': 'date-input',
-    'textarea-form-input': 'textarea-input',
-    'password-form-input': 'vendure:passwordInput',
-    'json-editor-form-input': 'vendure:jsonEditorInput',
-    'html-editor-form-input': 'vendure:htmlEditorInput',
-    'rich-text-form-input': 'vendure:richTextInput',
-    'boolean-form-input': 'boolean-input',
-    'select-form-input': 'select-input',
-    'text-form-input': 'vendure:textInput',
-    'product-multi-form-input': 'vendure:productMultiInput',
-    'combination-mode-form-input': 'vendure:combinationModeInput',
-    'relation-form-input': 'vendure:relationInput',
-    'struct-form-input': 'vendure:structInput',
-} as const;
-
 export function ConfigurableOperationArgInput({
     definition,
     value,
@@ -51,345 +17,20 @@ export function ConfigurableOperationArgInput({
     readOnly,
     position,
 }: Readonly<ConfigurableOperationArgInputProps>) {
-    const uiComponent = (definition.ui as any)?.component;
-    const argType = definition.type as ConfigArgType;
-    const isList = definition.list ?? false;
-
-    // Handle specific UI components first
-    if (uiComponent) {
-        switch (uiComponent) {
-            case 'product-selector-form-input': {
-                const entityType =
-                    (definition.ui as any)?.selectionMode === 'variant' ? 'ProductVariant' : 'Product';
-                const isMultiple = (definition.ui as any)?.multiple ?? false;
-                return (
-                    <DefaultRelationInput
-                        fieldDef={
-                            {
-                                entity: entityType,
-                                list: isMultiple,
-                            } as RelationCustomFieldConfig
-                        }
-                        field={{
-                            value,
-                            onChange,
-                            onBlur: () => {},
-                            name: '',
-                            ref: () => {},
-                        }}
-                        disabled={readOnly}
-                    />
-                );
-            }
-            case 'customer-group-form-input': {
-                const isCustomerGroupMultiple = (definition.ui as any)?.multiple ?? false;
-                return (
-                    <DefaultRelationInput
-                        fieldDef={
-                            {
-                                entity: 'CustomerGroup',
-                                list: isCustomerGroupMultiple,
-                            } as RelationCustomFieldConfig
-                        }
-                        field={{
-                            value,
-                            onChange,
-                            onBlur: () => {},
-                            name: '',
-                            ref: () => {},
-                        }}
-                        disabled={readOnly}
-                    />
-                );
-            }
-            case 'facet-value-form-input': {
-                return <FacetValueInput value={value} onChange={onChange} readOnly={readOnly} />;
-            }
-            case 'select-form-input': {
-                return (
-                    <SelectInput
-                        definition={definition}
-                        value={value}
-                        onChange={onChange}
-                        readOnly={readOnly}
-                    />
-                );
-            }
-            case 'textarea-form-input': {
-                return (
-                    <TextareaInput
-                        definition={definition}
-                        value={value}
-                        onChange={onChange}
-                        readOnly={readOnly}
-                    />
-                );
-            }
-            case 'date-form-input': {
-                return <DateTimeInput value={value} onChange={onChange} disabled={readOnly} />;
-            }
-            case 'boolean-form-input': {
-                return <BooleanInput value={value} onChange={onChange} readOnly={readOnly} />;
-            }
-            case 'number-form-input': {
-                return (
-                    <NumberInput
-                        definition={definition}
-                        value={value}
-                        onChange={onChange}
-                        readOnly={readOnly}
-                    />
-                );
-            }
-            case 'currency-form-input': {
-                return (
-                    <CurrencyInput
-                        definition={definition}
-                        value={value}
-                        onChange={onChange}
-                        readOnly={readOnly}
-                    />
-                );
-            }
-            default: {
-                // Try to use the component registry for other UI components
-                const componentId = UI_COMPONENT_MAP[uiComponent as keyof typeof UI_COMPONENT_MAP];
-                if (componentId) {
-                    try {
-                        return (
-                            <InputComponent
-                                id={componentId}
-                                value={value}
-                                onChange={onChange}
-                                readOnly={readOnly}
-                                position={position}
-                                definition={definition}
-                                {...(definition.ui as any)}
-                            />
-                        );
-                    } catch (error) {
-                        console.warn(
-                            `Failed to load UI component ${uiComponent}, falling back to type-based input`,
-                        );
-                    }
-                }
-            }
-        }
-    }
-
-    // Handle list fields with array wrapper
-    if (isList) {
-        return (
-            <ConfigurableOperationListInput
-                definition={definition}
-                value={value}
-                onChange={onChange}
-                readOnly={readOnly}
-            />
-        );
-    }
-
-    // Fall back to type-based rendering
-    switch (argType) {
-        case 'boolean':
-            return <BooleanInput value={value} onChange={onChange} readOnly={readOnly} />;
-
-        case 'int':
-        case 'float':
-            return (
-                <NumberInput definition={definition} value={value} onChange={onChange} readOnly={readOnly} />
-            );
-
-        case 'datetime':
-            return <DateTimeInput value={value} onChange={onChange} disabled={readOnly} />;
-
-        case 'ID':
-            // ID fields typically need specialized selectors
-            return (
-                <Input
-                    type="text"
-                    value={value || ''}
-                    onChange={e => onChange(e.target.value)}
-                    disabled={readOnly}
-                    placeholder="Enter ID..."
-                    className="bg-background"
-                />
-            );
-
-        case 'string':
-        default:
-            return (
-                <Input
-                    type="text"
-                    value={value || ''}
-                    onChange={e => onChange(e.target.value)}
-                    disabled={readOnly}
-                    className="bg-background"
-                />
-            );
-    }
-}
-
-/**
- * Boolean input component
- */
-function BooleanInput({
-    value,
-    onChange,
-    readOnly,
-}: Readonly<{
-    value: string;
-    onChange: (value: string) => void;
-    readOnly?: boolean;
-}>) {
-    const boolValue = value === 'true';
-
-    return (
-        <Switch
-            checked={boolValue}
-            onCheckedChange={checked => onChange(checked.toString())}
-            disabled={readOnly}
-        />
-    );
-}
-
-/**
- * Number input component with support for UI configuration
- */
-function NumberInput({
-    definition,
-    value,
-    onChange,
-    readOnly,
-}: Readonly<{
-    definition: ConfigurableOperationDefFragment['args'][number];
-    value: string;
-    onChange: (value: string) => void;
-    readOnly?: boolean;
-}>) {
-    const ui = definition.ui as any;
-    const isFloat = (definition.type as ConfigArgType) === 'float';
-    const min = ui?.min;
-    const max = ui?.max;
-    const step = ui?.step || (isFloat ? 0.01 : 1);
-    const prefix = ui?.prefix;
-    const suffix = ui?.suffix;
-
-    const numericValue = value ? parseFloat(value) : '';
-
-    return (
-        <AffixedInput
-            type="number"
-            value={numericValue}
-            onChange={e => {
-                const val = e.target.valueAsNumber;
-                onChange(isNaN(val) ? '' : val.toString());
-            }}
-            disabled={readOnly}
-            min={min}
-            max={max}
-            step={step}
-            prefix={prefix}
-            suffix={suffix}
-            className="bg-background"
-        />
-    );
-}
-
-/**
- * Currency input component
- */
-function CurrencyInput({
-    definition,
-    value,
-    onChange,
-    readOnly,
-}: Readonly<{
-    definition: ConfigurableOperationDefFragment['args'][number];
-    value: string;
-    onChange: (value: string) => void;
-    readOnly?: boolean;
-}>) {
-    const numericValue = value ? parseInt(value, 10) : '';
-
+    const universalFieldDef = configArgToUniversal(definition);
     return (
-        <AffixedInput
-            type="number"
-            value={numericValue}
-            onChange={e => {
-                const val = e.target.valueAsNumber;
-                onChange(isNaN(val) ? '0' : val.toString());
+        <UniversalFormInput
+            fieldDef={universalFieldDef}
+            field={{
+                value,
+                onChange,
+                onBlur: () => {},
+                name: definition.name,
+                ref: () => {},
             }}
+            valueMode="json-string"
             disabled={readOnly}
-            min={0}
-            step={1}
-            prefix="$"
-            className="bg-background"
-        />
-    );
-}
-
-/**
- * Select input component with options
- */
-function SelectInput({
-    definition,
-    value,
-    onChange,
-    readOnly,
-}: Readonly<{
-    definition: ConfigurableOperationDefFragment['args'][number];
-    value: string;
-    onChange: (value: string) => void;
-    readOnly?: boolean;
-}>) {
-    const ui = definition.ui as any;
-    const options = ui?.options || [];
-
-    return (
-        <Select value={value} onValueChange={onChange} disabled={readOnly}>
-            <SelectTrigger className="bg-background mb-0">
-                <SelectValue placeholder="Select an option..." />
-            </SelectTrigger>
-            <SelectContent>
-                {options.map((option: any) => (
-                    <SelectItem key={option.value} value={option.value}>
-                        {typeof option.label === 'string'
-                            ? option.label
-                            : option.label?.[0]?.value || option.value}
-                    </SelectItem>
-                ))}
-            </SelectContent>
-        </Select>
-    );
-}
-
-/**
- * Textarea input component
- */
-function TextareaInput({
-    definition,
-    value,
-    onChange,
-    readOnly,
-}: Readonly<{
-    definition: ConfigurableOperationDefFragment['args'][number];
-    value: string;
-    onChange: (value: string) => void;
-    readOnly?: boolean;
-}>) {
-    const ui = definition.ui as any;
-    const spellcheck = ui?.spellcheck ?? true;
-
-    return (
-        <Textarea
-            value={value || ''}
-            onChange={e => onChange(e.target.value)}
-            disabled={readOnly}
-            spellCheck={spellcheck}
-            placeholder="Enter text..."
-            rows={4}
-            className="bg-background"
+            position={position}
         />
     );
 }

+ 19 - 143
packages/dashboard/src/lib/components/shared/custom-fields-form.tsx

@@ -1,7 +1,4 @@
 import { CustomFieldListInput } from '@/vdb/components/data-input/custom-field-list-input.js';
-import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
-import { DefaultRelationInput } from '@/vdb/components/data-input/default-relation-input.js';
-import { SelectWithOptions } from '@/vdb/components/data-input/select-with-options.js';
 import { StructFormInput } from '@/vdb/components/data-input/struct-form-input.js';
 import {
     FormControl,
@@ -11,20 +8,18 @@ import {
     FormLabel,
     FormMessage,
 } from '@/vdb/components/ui/form.js';
-import { Input } from '@/vdb/components/ui/input.js';
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/vdb/components/ui/tabs.js';
 import { CustomFormComponent } from '@/vdb/framework/form-engine/custom-form-component.js';
 import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
 import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
 import { useLingui } from '@/vdb/lib/trans.js';
 import { customFieldConfigFragment } from '@/vdb/providers/server-config.js';
-import { StringCustomFieldConfig } from '@vendure/common/lib/generated-types';
-import { CustomFieldType } from '@vendure/common/lib/shared-types';
 import { ResultOf } from 'gql.tada';
 import React, { useMemo } from 'react';
-import { Control, ControllerRenderProps } from 'react-hook-form';
-import { Switch } from '../ui/switch.js';
+import { Control } from 'react-hook-form';
 import { TranslatableFormField } from './translatable-form-field.js';
+import { customFieldToUniversal } from './universal-field-definition.js';
+import { UniversalFormInput } from './universal-form-input.js';
 
 type CustomFieldConfig = ResultOf<typeof customFieldConfigFragment>;
 
@@ -174,7 +169,14 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
                                         }}
                                     />
                                 ) : (
-                                    <FormInputForType fieldDef={fieldDef} field={field} />
+                                    <UniversalFormInput
+                                        fieldDef={customFieldToUniversal(fieldDef)}
+                                        field={field}
+                                        valueMode="native"
+                                        disabled={fieldDef.readonly ?? false}
+                                        control={control}
+                                        getTranslation={getTranslation}
+                                    />
                                 )}
                             </FormControl>
                             <FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
@@ -293,7 +295,14 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
                         getTranslation={getTranslation}
                         fieldName={fieldDef.name}
                     >
-                        <FormInputForType fieldDef={fieldDef} field={field} />
+                        <UniversalFormInput
+                            fieldDef={customFieldToUniversal(fieldDef)}
+                            field={field}
+                            valueMode="native"
+                            disabled={fieldDef.readonly ?? false}
+                            control={control}
+                            getTranslation={getTranslation}
+                        />
                     </CustomFieldFormItem>
                 )}
             />
@@ -325,136 +334,3 @@ function CustomFieldFormItem({
         </FormItem>
     );
 }
-
-function FormInputForType({
-    fieldDef,
-    field,
-}: Readonly<{
-    fieldDef: CustomFieldConfig;
-    field: ControllerRenderProps<any, any>;
-}>) {
-    const isReadonly = fieldDef.readonly ?? false;
-    const isList = fieldDef.list ?? false;
-
-    // Helper function to render individual input components
-    const renderSingleInput = (inputField: ControllerRenderProps<any, any>) => {
-        switch (fieldDef.type as CustomFieldType) {
-            case 'float':
-            case 'int': {
-                const numericFieldDef = fieldDef as any;
-                const isFloat = fieldDef.type === 'float';
-                const min = isFloat ? numericFieldDef.floatMin : numericFieldDef.intMin;
-                const max = isFloat ? numericFieldDef.floatMax : numericFieldDef.intMax;
-                const step = isFloat ? numericFieldDef.floatStep : numericFieldDef.intStep;
-
-                return (
-                    <Input
-                        type="number"
-                        value={inputField.value ?? ''}
-                        onChange={e => {
-                            const value = e.target.valueAsNumber;
-                            inputField.onChange(isNaN(value) ? undefined : value);
-                        }}
-                        onBlur={inputField.onBlur}
-                        name={inputField.name}
-                        disabled={isReadonly}
-                        min={min}
-                        max={max}
-                        step={step}
-                    />
-                );
-            }
-            case 'boolean':
-                return (
-                    <Switch
-                        checked={inputField.value}
-                        onCheckedChange={inputField.onChange}
-                        disabled={isReadonly}
-                    />
-                );
-            case 'datetime': {
-                return (
-                    <DateTimeInput
-                        value={inputField.value}
-                        onChange={inputField.onChange}
-                        disabled={isReadonly}
-                    />
-                );
-            }
-            case 'struct':
-                // Struct fields need special handling and can't be rendered as simple inputs
-                return null;
-            case 'string':
-            default:
-                return (
-                    <Input
-                        value={inputField.value ?? ''}
-                        onChange={e => inputField.onChange(e.target.value)}
-                        onBlur={inputField.onBlur}
-                        name={inputField.name}
-                        disabled={isReadonly}
-                    />
-                );
-        }
-    };
-
-    // Handle struct fields with special component
-    if (fieldDef.type === 'struct') {
-        // We need access to the control and getTranslation function
-        // This will need to be passed down from the parent component
-        return null; // Placeholder - struct fields are handled differently in the parent
-    }
-
-    // Handle relation fields directly (they handle list/single internally)
-    if (fieldDef.type === 'relation') {
-        return <DefaultRelationInput fieldDef={fieldDef as any} field={field} disabled={isReadonly} />;
-    }
-
-    // Handle string fields with options (dropdown) - already handles list case with multi-select
-    if (fieldDef.type === 'string') {
-        const options = (fieldDef as StringCustomFieldConfig).options;
-        if (options && options.length > 0) {
-            return (
-                <SelectWithOptions
-                    field={field}
-                    options={options}
-                    disabled={isReadonly}
-                    isListField={isList}
-                />
-            );
-        }
-    }
-
-    // For list fields (except string with options and relations which are handled above), wrap with list input
-    if (isList) {
-        const getDefaultValue = () => {
-            switch (fieldDef.type as CustomFieldType) {
-                case 'string':
-                    return '';
-                case 'int':
-                case 'float':
-                    return 0;
-                case 'boolean':
-                    return false;
-                case 'datetime':
-                    return '';
-                case 'relation':
-                    return '';
-                default:
-                    return '';
-            }
-        };
-
-        return (
-            <CustomFieldListInput
-                field={field}
-                disabled={isReadonly}
-                renderInput={(index, inputField) => renderSingleInput(inputField)}
-                defaultValue={getDefaultValue()}
-            />
-        );
-    }
-
-    // For non-list fields, render directly
-    return renderSingleInput(field);
-}

+ 393 - 0
packages/dashboard/src/lib/components/shared/direct-form-component-map.tsx

@@ -0,0 +1,393 @@
+import { DefaultFormComponentId } from '@vendure/common/lib/shared-types';
+import React from 'react';
+
+import { AffixedInput } from '@/vdb/components/data-input/affixed-input.js';
+import { CombinationModeInput } from '@/vdb/components/data-input/combination-mode-input.js';
+import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
+import { DefaultRelationInput } from '@/vdb/components/data-input/default-relation-input.js';
+import { FacetValueInput } from '@/vdb/components/data-input/facet-value-input.js';
+import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
+import { ProductMultiInput } from '@/vdb/components/data-input/product-multi-selector.js';
+import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
+import { Input } from '@/vdb/components/ui/input.js';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
+import { Switch } from '@/vdb/components/ui/switch.js';
+import { Textarea } from '@/vdb/components/ui/textarea.js';
+
+import { UniversalFieldDefinition } from './universal-field-definition.js';
+import { transformValue, ValueMode } from './value-transformers.js';
+
+/**
+ * Custom hook to handle value transformation between native and JSON string modes
+ * Eliminates duplication across form input components
+ */
+function useValueTransformation(
+    field: { value: any; onChange: (value: any) => void },
+    fieldDef: UniversalFieldDefinition,
+    valueMode: ValueMode,
+) {
+    const transformedValue = React.useMemo(() => {
+        return valueMode === 'json-string'
+            ? transformValue(field.value, fieldDef, valueMode, 'parse')
+            : field.value;
+    }, [field.value, fieldDef, valueMode]);
+
+    const handleChange = React.useCallback(
+        (newValue: any) => {
+            const serializedValue =
+                valueMode === 'json-string'
+                    ? transformValue(newValue, fieldDef, valueMode, 'serialize')
+                    : newValue;
+            field.onChange(serializedValue);
+        },
+        [field.onChange, fieldDef, valueMode],
+    );
+
+    return { transformedValue, handleChange };
+}
+
+export interface DirectFormComponentProps {
+    fieldDef: UniversalFieldDefinition;
+    field: {
+        value: any;
+        onChange: (value: any) => void;
+        onBlur?: () => void;
+        name: string;
+        ref?: any;
+    };
+    valueMode: ValueMode;
+    disabled?: boolean;
+}
+
+/**
+ * Text input wrapper for config args
+ */
+const TextFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    const handleChange = React.useCallback(
+        (e: React.ChangeEvent<HTMLInputElement>) => {
+            // For both modes, text values are stored as strings
+            field.onChange(e.target.value);
+        },
+        [field.onChange],
+    );
+
+    const value = field.value || '';
+
+    return (
+        <Input
+            type="text"
+            value={value}
+            onChange={handleChange}
+            onBlur={field.onBlur}
+            name={field.name}
+            disabled={disabled}
+            className={valueMode === 'json-string' ? 'bg-background' : undefined}
+        />
+    );
+};
+
+/**
+ * Number input wrapper for config args
+ */
+const NumberFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    const ui = fieldDef.ui;
+    const isFloat = fieldDef.type === 'float';
+    const min = ui?.min;
+    const max = ui?.max;
+    const step = ui?.step || (isFloat ? 0.01 : 1);
+    const prefix = ui?.prefix;
+    const suffix = ui?.suffix;
+
+    const handleChange = React.useCallback(
+        (newValue: number | '') => {
+            if (valueMode === 'json-string') {
+                // For config args, store as string
+                field.onChange(newValue === '' ? '' : newValue.toString());
+            } else {
+                // For custom fields, store as number or undefined
+                field.onChange(newValue === '' ? undefined : newValue);
+            }
+        },
+        [field.onChange, valueMode],
+    );
+
+    // Parse current value to number
+    const numericValue = React.useMemo(() => {
+        if (field.value === undefined || field.value === null || field.value === '') {
+            return '';
+        }
+        const parsed = typeof field.value === 'number' ? field.value : parseFloat(field.value);
+        return isNaN(parsed) ? '' : parsed;
+    }, [field.value]);
+
+    // Use AffixedInput if we have prefix/suffix or for config args mode
+    if (prefix || suffix || valueMode === 'json-string') {
+        return (
+            <AffixedInput
+                type="number"
+                value={numericValue}
+                onChange={e => {
+                    const val = e.target.valueAsNumber;
+                    handleChange(isNaN(val) ? '' : val);
+                }}
+                disabled={disabled}
+                min={min}
+                max={max}
+                step={step}
+                prefix={prefix}
+                suffix={suffix}
+                className="bg-background"
+            />
+        );
+    }
+
+    return (
+        <Input
+            type="number"
+            value={numericValue}
+            onChange={e => {
+                const val = e.target.valueAsNumber;
+                handleChange(isNaN(val) ? '' : val);
+            }}
+            onBlur={field.onBlur}
+            name={field.name}
+            disabled={disabled}
+            min={min}
+            max={max}
+            step={step}
+        />
+    );
+};
+
+/**
+ * Boolean input wrapper
+ */
+const BooleanFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    // Parse the current value to boolean
+    const currentValue = React.useMemo(() => {
+        if (valueMode === 'json-string') {
+            return field.value === 'true' || field.value === true;
+        } else {
+            return Boolean(field.value);
+        }
+    }, [field.value, valueMode]);
+
+    // Simple change handler - directly call field.onChange
+    const handleChange = React.useCallback(
+        (newValue: boolean) => {
+            if (valueMode === 'json-string') {
+                field.onChange(newValue.toString());
+            } else {
+                field.onChange(newValue);
+            }
+        },
+        [field.onChange, valueMode],
+    );
+
+    return <Switch checked={currentValue} onCheckedChange={handleChange} disabled={disabled} />;
+};
+
+/**
+ * Currency input wrapper (uses MoneyInput)
+ */
+const CurrencyFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
+
+    return <MoneyInput value={transformedValue} onChange={handleChange} disabled={disabled} />;
+};
+
+/**
+ * Date input wrapper
+ */
+const DateFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
+
+    return <DateTimeInput value={transformedValue} onChange={handleChange} disabled={disabled} />;
+};
+
+/**
+ * Select input wrapper
+ */
+const SelectFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
+    const options = fieldDef.ui?.options || [];
+
+    return (
+        <Select value={transformedValue || ''} onValueChange={handleChange} disabled={disabled}>
+            <SelectTrigger className="bg-background mb-0">
+                <SelectValue placeholder="Select an option..." />
+            </SelectTrigger>
+            <SelectContent>
+                {options.map(option => (
+                    <SelectItem key={option.value} value={option.value}>
+                        {typeof option.label === 'string'
+                            ? option.label
+                            : Array.isArray(option.label)
+                              ? option.label[0]?.value || option.value
+                              : option.value}
+                    </SelectItem>
+                ))}
+            </SelectContent>
+        </Select>
+    );
+};
+
+/**
+ * Textarea input wrapper
+ */
+const TextareaFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
+
+    const handleTextareaChange = React.useCallback(
+        (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+            handleChange(e.target.value);
+        },
+        [handleChange],
+    );
+
+    return (
+        <Textarea
+            value={transformedValue || ''}
+            onChange={handleTextareaChange}
+            disabled={disabled}
+            spellCheck={fieldDef.ui?.spellcheck ?? true}
+            placeholder="Enter text..."
+            rows={4}
+            className="bg-background"
+        />
+    );
+};
+
+/**
+ * Product selector wrapper (uses DefaultRelationInput)
+ */
+const ProductSelectorFormInput: React.FC<DirectFormComponentProps> = ({
+    field,
+    disabled,
+    fieldDef,
+    valueMode,
+}) => {
+    const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
+    const entityType = fieldDef.ui?.selectionMode === 'variant' ? 'ProductVariant' : 'Product';
+
+    return (
+        <DefaultRelationInput
+            fieldDef={
+                {
+                    entity: entityType,
+                    list: fieldDef.list,
+                } as any
+            }
+            field={{
+                ...field,
+                value: transformedValue,
+                onChange: handleChange,
+            }}
+            disabled={disabled}
+        />
+    );
+};
+
+/**
+ * Customer group input wrapper
+ */
+const CustomerGroupFormInput: React.FC<DirectFormComponentProps> = ({
+    field,
+    disabled,
+    fieldDef,
+    valueMode,
+}) => {
+    const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
+
+    return (
+        <DefaultRelationInput
+            fieldDef={
+                {
+                    entity: 'CustomerGroup',
+                    list: fieldDef.list,
+                } as any
+            }
+            field={{
+                ...field,
+                value: transformedValue,
+                onChange: handleChange,
+            }}
+            disabled={disabled}
+        />
+    );
+};
+
+/**
+ * Password input wrapper (uses regular Input with type="password")
+ */
+const PasswordFormInput: React.FC<DirectFormComponentProps> = ({ field, disabled, fieldDef, valueMode }) => {
+    const { transformedValue, handleChange } = useValueTransformation(field, fieldDef, valueMode);
+
+    const handleInputChange = React.useCallback(
+        (e: React.ChangeEvent<HTMLInputElement>) => {
+            handleChange(e.target.value);
+        },
+        [handleChange],
+    );
+
+    return (
+        <Input
+            type="password"
+            value={transformedValue || ''}
+            onChange={handleInputChange}
+            onBlur={field.onBlur}
+            name={field.name}
+            disabled={disabled}
+            className={valueMode === 'json-string' ? 'bg-background' : undefined}
+        />
+    );
+};
+
+/**
+ * Direct mapping from DefaultFormComponentId to React components
+ * This eliminates the need for intermediate registry IDs
+ */
+export const DIRECT_FORM_COMPONENT_MAP: Record<DefaultFormComponentId, React.FC<DirectFormComponentProps>> = {
+    'boolean-form-input': BooleanFormInput,
+    'currency-form-input': CurrencyFormInput,
+    'customer-group-form-input': CustomerGroupFormInput,
+    'date-form-input': DateFormInput,
+    'facet-value-form-input': ({ field, disabled }) => (
+        <FacetValueInput value={field.value} onChange={field.onChange} readOnly={disabled} />
+    ),
+    'json-editor-form-input': TextareaFormInput, // Fallback to textarea for now
+    'html-editor-form-input': ({ field, disabled }) => (
+        <RichTextInput value={field.value} onChange={field.onChange} disabled={disabled} />
+    ),
+    'number-form-input': NumberFormInput,
+    'password-form-input': PasswordFormInput,
+    'product-selector-form-input': ProductSelectorFormInput,
+    'relation-form-input': ProductSelectorFormInput, // Uses same relation logic
+    'rich-text-form-input': ({ field, disabled }) => (
+        <RichTextInput value={field.value} onChange={field.onChange} disabled={disabled} />
+    ),
+    'select-form-input': SelectFormInput,
+    'text-form-input': TextFormInput,
+    'textarea-form-input': TextareaFormInput,
+    'product-multi-form-input': ({ field, disabled, fieldDef }) => (
+        <ProductMultiInput
+            value={field.value}
+            onChange={field.onChange}
+            disabled={disabled}
+            selectionMode={fieldDef.ui?.selectionMode as any}
+        />
+    ),
+    'combination-mode-form-input': ({ field, disabled }) => (
+        <CombinationModeInput value={field.value} onChange={field.onChange} disabled={disabled} />
+    ),
+    'struct-form-input': TextareaFormInput, // Fallback for now
+};
+
+/**
+ * Get a direct form component by ID
+ */
+export function getDirectFormComponent(
+    componentId: DefaultFormComponentId,
+): React.FC<DirectFormComponentProps> | undefined {
+    return DIRECT_FORM_COMPONENT_MAP[componentId];
+}

+ 118 - 0
packages/dashboard/src/lib/components/shared/universal-field-definition.ts

@@ -0,0 +1,118 @@
+import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
+import { CustomFieldConfig } from '@vendure/common/lib/generated-types';
+import { ConfigArgType, CustomFieldType, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
+
+/**
+ * Universal field definition that can represent both custom fields and configurable operation args
+ */
+export interface UniversalFieldDefinition {
+    name: string;
+    type: CustomFieldType | 'ID'; // Extends CustomFieldType with ID for config args
+    list?: boolean;
+    readonly?: boolean;
+    ui?: {
+        component?: DefaultFormComponentId | string;
+        options?: Array<{ value: string; label: string | Array<{ languageCode: string; value: string }> }>;
+        min?: number;
+        max?: number;
+        step?: number;
+        prefix?: string;
+        suffix?: string;
+        tab?: string;
+        fullWidth?: boolean;
+        spellcheck?: boolean;
+        selectionMode?: string;
+    };
+    entity?: string; // for relations
+    label?: string | Array<{ languageCode: string; value: string }>;
+    description?: string | Array<{ languageCode: string; value: string }>;
+}
+
+/**
+ * Convert a custom field config to universal field definition
+ */
+export function customFieldToUniversal(fieldDef: CustomFieldConfig): UniversalFieldDefinition {
+    const hasOptions = (fieldDef as any).options;
+    const hasUi = fieldDef.ui;
+    const hasNumericConfig =
+        (fieldDef as any).min !== undefined ||
+        (fieldDef as any).max !== undefined ||
+        (fieldDef as any).step !== undefined;
+
+    return {
+        name: fieldDef.name,
+        type: fieldDef.type as any,
+        list: fieldDef.list ?? false,
+        readonly: fieldDef.readonly ?? false,
+        ui:
+            hasUi || hasOptions || hasNumericConfig
+                ? {
+                      component: fieldDef.ui?.component,
+                      options: (fieldDef as any).options,
+                      ...((fieldDef as any).min != null && {
+                          min: (fieldDef as any).min,
+                      }),
+                      ...((fieldDef as any).max != null && {
+                          max: (fieldDef as any).max,
+                      }),
+                      ...((fieldDef as any).step != null && {
+                          step: (fieldDef as any).step,
+                      }),
+                      tab: fieldDef.ui?.tab,
+                      fullWidth: fieldDef.ui?.fullWidth,
+                  }
+                : undefined,
+        entity: (fieldDef as any).entity,
+        label: fieldDef.label,
+        description: fieldDef.description,
+    };
+}
+
+/**
+ * Convert a configurable operation arg definition to universal field definition
+ */
+export function configArgToUniversal(
+    definition: ConfigurableOperationDefFragment['args'][number],
+): UniversalFieldDefinition {
+    const ui = definition.ui;
+
+    return {
+        name: definition.name,
+        type: mapConfigArgType(definition.type as ConfigArgType),
+        list: definition.list ?? false,
+        readonly: false,
+        ui: ui
+            ? {
+                  component: ui.component,
+                  options: ui.options,
+                  min: ui.min ?? undefined,
+                  max: ui.max ?? undefined,
+                  step: ui.step ?? undefined,
+                  prefix: ui.prefix,
+                  suffix: ui.suffix,
+                  spellcheck: ui.spellcheck,
+                  selectionMode: ui.selectionMode,
+              }
+            : undefined,
+        entity: getEntityFromUiComponent(ui?.component),
+        label: definition.label,
+        description: definition.description,
+    };
+}
+
+function mapConfigArgType(configArgType: ConfigArgType): UniversalFieldDefinition['type'] {
+    // All ConfigArgType values are compatible with our extended type
+    return configArgType as UniversalFieldDefinition['type'];
+}
+
+function getEntityFromUiComponent(component?: string): string | undefined {
+    switch (component) {
+        case 'product-selector-form-input':
+        case 'product-multi-form-input':
+            return 'Product';
+        case 'customer-group-form-input':
+            return 'CustomerGroup';
+        default:
+            return undefined;
+    }
+}

+ 175 - 0
packages/dashboard/src/lib/components/shared/universal-form-input.tsx

@@ -0,0 +1,175 @@
+import { DefaultFormComponentId } from '@vendure/common/lib/shared-types';
+import { ControllerRenderProps } from 'react-hook-form';
+
+import { CustomFieldListInput } from '@/vdb/components/data-input/custom-field-list-input.js';
+import { StructFormInput } from '@/vdb/components/data-input/struct-form-input.js';
+import {
+    CustomFormComponent,
+    CustomFormComponentInputProps,
+} from '@/vdb/framework/form-engine/custom-form-component.js';
+
+import { ConfigurableOperationListInput } from '../data-input/configurable-operation-list-input.js';
+import { FacetValueInput } from '../data-input/facet-value-input.js';
+import { getDirectFormComponent } from './direct-form-component-map.js';
+import { UniversalFieldDefinition } from './universal-field-definition.js';
+import { UniversalInputComponent } from './universal-input-components.js';
+import { ValueMode } from './value-transformers.js';
+
+export interface UniversalFormInputProps {
+    fieldDef: UniversalFieldDefinition;
+    field: ControllerRenderProps<any, any>;
+    valueMode: ValueMode;
+    disabled?: boolean;
+    // Additional props for config args mode
+    position?: number;
+    // Additional props for custom fields mode
+    control?: any;
+    getTranslation?: (
+        input: Array<{ languageCode: string; value: string }> | null | undefined,
+    ) => string | undefined;
+}
+
+/**
+ * Universal form input component that handles both custom fields and configurable operation args
+ * Maintains full backward compatibility with existing APIs while eliminating duplication
+ */
+export function UniversalFormInput({
+    fieldDef,
+    field,
+    valueMode,
+    disabled = false,
+    position,
+    control,
+    getTranslation,
+}: Readonly<UniversalFormInputProps>) {
+    const uiComponent = fieldDef.ui?.component;
+    const isList = fieldDef.list ?? false;
+    const isReadonly = disabled || fieldDef.readonly;
+
+    // Handle special case: facet-value-form-input (only in config args)
+    if (uiComponent === 'facet-value-form-input' && valueMode === 'json-string') {
+        return <FacetValueInput value={field.value} onChange={field.onChange} readOnly={isReadonly} />;
+    }
+
+    // Handle custom form components (custom fields mode)
+    if (uiComponent && valueMode === 'native') {
+        const fieldProps: CustomFormComponentInputProps = {
+            field: {
+                ...field,
+                disabled: isReadonly,
+            },
+            fieldState: {} as any, // This would be passed from the parent FormField
+            formState: {} as any, // This would be passed from the parent FormField
+        };
+
+        return <CustomFormComponent fieldDef={fieldDef as any} fieldProps={fieldProps} />;
+    }
+
+    // Handle direct component mapping (config args mode)
+    if (uiComponent && valueMode === 'json-string') {
+        const DirectComponent = getDirectFormComponent(uiComponent as DefaultFormComponentId);
+        if (DirectComponent) {
+            return (
+                <DirectComponent
+                    fieldDef={fieldDef}
+                    field={field}
+                    valueMode={valueMode}
+                    disabled={isReadonly}
+                />
+            );
+        }
+    }
+
+    // Handle struct fields (custom fields mode only)
+    if (fieldDef.type === 'struct' && valueMode === 'native') {
+        if (isList) {
+            return (
+                <CustomFieldListInput
+                    field={field}
+                    disabled={isReadonly}
+                    renderInput={(index, inputField) => (
+                        <StructFormInput
+                            field={inputField}
+                            fieldDef={fieldDef as any}
+                            control={control}
+                            getTranslation={getTranslation}
+                        />
+                    )}
+                    defaultValue={{}}
+                    isFullWidth={true}
+                />
+            );
+        }
+
+        return (
+            <StructFormInput
+                field={field}
+                fieldDef={fieldDef as any}
+                control={control}
+                getTranslation={getTranslation}
+            />
+        );
+    }
+
+    // Handle list fields
+    if (isList) {
+        if (valueMode === 'json-string') {
+            // Use ConfigurableOperationListInput for config args
+            return (
+                <ConfigurableOperationListInput
+                    definition={fieldDef as any}
+                    value={field.value}
+                    onChange={field.onChange}
+                    readOnly={isReadonly}
+                />
+            );
+        } else {
+            // Use CustomFieldListInput for custom fields
+            const getDefaultValue = () => {
+                switch (fieldDef.type) {
+                    case 'string':
+                    case 'localeString':
+                    case 'localeText':
+                        return '';
+                    case 'int':
+                    case 'float':
+                        return 0;
+                    case 'boolean':
+                        return false;
+                    case 'datetime':
+                        return '';
+                    case 'relation':
+                        return '';
+                    default:
+                        return '';
+                }
+            };
+
+            return (
+                <CustomFieldListInput
+                    field={field}
+                    disabled={isReadonly}
+                    renderInput={(index, inputField) => (
+                        <UniversalInputComponent
+                            fieldDef={{ ...fieldDef, list: false }}
+                            field={inputField}
+                            valueMode={valueMode}
+                            disabled={isReadonly}
+                        />
+                    )}
+                    defaultValue={getDefaultValue()}
+                />
+            );
+        }
+    }
+
+    // Fall back to consolidated input component
+    return (
+        <UniversalInputComponent
+            fieldDef={fieldDef}
+            field={field}
+            valueMode={valueMode}
+            disabled={isReadonly}
+        />
+    );
+}

+ 291 - 0
packages/dashboard/src/lib/components/shared/universal-input-components.tsx

@@ -0,0 +1,291 @@
+import React from 'react';
+import { ControllerRenderProps } from 'react-hook-form';
+
+import { AffixedInput } from '../data-input/affixed-input.js';
+import { DateTimeInput } from '../data-input/datetime-input.js';
+import { DefaultRelationInput } from '../data-input/default-relation-input.js';
+import { SelectWithOptions } from '../data-input/select-with-options.js';
+import { Input } from '../ui/input.js';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
+import { Switch } from '../ui/switch.js';
+import { Textarea } from '../ui/textarea.js';
+import { UniversalFieldDefinition } from './universal-field-definition.js';
+import { ValueMode, transformValue } from './value-transformers.js';
+
+export interface UniversalInputComponentProps {
+    fieldDef: UniversalFieldDefinition;
+    field: ControllerRenderProps<any, any>;
+    valueMode: ValueMode;
+    disabled?: boolean;
+}
+
+// Component renderer interface for cleaner separation
+interface ComponentRendererProps {
+    fieldDef: UniversalFieldDefinition;
+    field: ControllerRenderProps<any, any>;
+    valueMode: ValueMode;
+    isReadonly: boolean;
+    transformedValue: any;
+    handleChange: (value: any) => void;
+    handleNumericChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+    handleRegularNumericChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+    handleTextareaChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
+    handleTextChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+}
+
+/**
+ * Renders relation input component
+ */
+function renderRelationInput({ fieldDef, field, transformedValue, handleChange, isReadonly }: ComponentRendererProps) {
+    if (fieldDef.type !== 'relation' || !fieldDef.entity) return null;
+    
+    return (
+        <DefaultRelationInput
+            fieldDef={{
+                entity: fieldDef.entity,
+                list: fieldDef.list,
+            } as any}
+            field={{
+                ...field,
+                value: transformedValue,
+                onChange: handleChange,
+            }}
+            disabled={isReadonly}
+        />
+    );
+}
+
+/**
+ * Renders string field with options as select dropdown
+ */
+function renderSelectInput({ fieldDef, valueMode, transformedValue, handleChange, isReadonly, field }: ComponentRendererProps) {
+    if (fieldDef.type !== 'string' || !fieldDef.ui?.options) return null;
+
+    if (valueMode === 'json-string') {
+        return (
+            <Select value={transformedValue || ''} onValueChange={handleChange} disabled={isReadonly}>
+                <SelectTrigger className="bg-background mb-0">
+                    <SelectValue placeholder="Select an option..." />
+                </SelectTrigger>
+                <SelectContent>
+                    {fieldDef.ui.options.map((option) => (
+                        <SelectItem key={option.value} value={option.value}>
+                            {typeof option.label === 'string'
+                                ? option.label
+                                : Array.isArray(option.label)
+                                ? option.label[0]?.value || option.value
+                                : option.value}
+                        </SelectItem>
+                    ))}
+                </SelectContent>
+            </Select>
+        );
+    }
+
+    return (
+        <SelectWithOptions
+            field={{
+                ...field,
+                value: transformedValue,
+                onChange: handleChange,
+            }}
+            options={fieldDef.ui.options as any}
+            disabled={isReadonly}
+            isListField={fieldDef.list}
+        />
+    );
+}
+
+/**
+ * Renders numeric input components (int/float)
+ */
+function renderNumericInput({ fieldDef, valueMode, transformedValue, handleNumericChange, handleRegularNumericChange, isReadonly, field }: ComponentRendererProps) {
+    if (fieldDef.type !== 'int' && fieldDef.type !== 'float') return null;
+
+    const isFloat = fieldDef.type === 'float';
+    const min = fieldDef.ui?.min;
+    const max = fieldDef.ui?.max;
+    const step = fieldDef.ui?.step || (isFloat ? 0.01 : 1);
+    const prefix = fieldDef.ui?.prefix;
+    const suffix = fieldDef.ui?.suffix;
+
+    const shouldUseAffixedInput = prefix || suffix || valueMode === 'json-string';
+
+    if (shouldUseAffixedInput) {
+        const numericValue = transformedValue !== undefined && transformedValue !== '' 
+            ? (typeof transformedValue === 'number' ? transformedValue : parseFloat(transformedValue) || '') 
+            : '';
+
+        return (
+            <AffixedInput
+                type="number"
+                value={numericValue}
+                onChange={handleNumericChange}
+                disabled={isReadonly}
+                min={min}
+                max={max}
+                step={step}
+                prefix={prefix}
+                suffix={suffix}
+                className="bg-background"
+            />
+        );
+    }
+
+    return (
+        <Input
+            type="number"
+            value={transformedValue ?? ''}
+            onChange={handleRegularNumericChange}
+            onBlur={field.onBlur}
+            name={field.name}
+            disabled={isReadonly}
+            min={min}
+            max={max}
+            step={step}
+        />
+    );
+}
+
+/**
+ * Renders boolean input as switch
+ */
+function renderBooleanInput({ fieldDef, valueMode, transformedValue, handleChange, isReadonly }: ComponentRendererProps) {
+    if (fieldDef.type !== 'boolean') return null;
+
+    const boolValue = valueMode === 'json-string' 
+        ? (transformedValue === true || transformedValue === 'true')
+        : transformedValue;
+
+    return (
+        <Switch
+            checked={boolValue}
+            onCheckedChange={handleChange}
+            disabled={isReadonly}
+        />
+    );
+}
+
+/**
+ * Renders datetime input
+ */
+function renderDateTimeInput({ fieldDef, transformedValue, handleChange, isReadonly }: ComponentRendererProps) {
+    if (fieldDef.type !== 'datetime') return null;
+
+    return (
+        <DateTimeInput
+            value={transformedValue}
+            onChange={handleChange}
+            disabled={isReadonly}
+        />
+    );
+}
+
+/**
+ * Renders textarea for specific config args
+ */
+function renderTextareaInput({ fieldDef, valueMode, transformedValue, handleTextareaChange, isReadonly }: ComponentRendererProps) {
+    if (valueMode !== 'json-string' || fieldDef.ui?.component !== 'textarea-form-input') return null;
+
+    return (
+        <Textarea
+            value={transformedValue || ''}
+            onChange={handleTextareaChange}
+            disabled={isReadonly}
+            spellCheck={fieldDef.ui?.spellcheck ?? true}
+            placeholder="Enter text..."
+            rows={4}
+            className="bg-background"
+        />
+    );
+}
+
+/**
+ * Renders default text input
+ */
+function renderTextInput({ valueMode, transformedValue, handleTextChange, isReadonly, field }: ComponentRendererProps) {
+    return (
+        <Input
+            type="text"
+            value={transformedValue ?? ''}
+            onChange={handleTextChange}
+            onBlur={field.onBlur}
+            name={field.name}
+            disabled={isReadonly}
+            placeholder={valueMode === 'json-string' ? "Enter value..." : undefined}
+            className={valueMode === 'json-string' ? "bg-background" : undefined}
+        />
+    );
+}
+
+/**
+ * Consolidated input component for rendering form inputs based on field type
+ * This replaces the duplicate implementations in custom fields and config args
+ */
+export function UniversalInputComponent({
+    fieldDef,
+    field,
+    valueMode,
+    disabled = false,
+}: Readonly<UniversalInputComponentProps>) {
+    const isReadonly = disabled || fieldDef.readonly;
+
+    // Transform the field value for the component
+    const transformedValue = React.useMemo(() => {
+        return valueMode === 'json-string' 
+            ? transformValue(field.value, fieldDef, valueMode, 'parse')
+            : field.value;
+    }, [field.value, fieldDef, valueMode]);
+
+    // Transform onChange handler for the component
+    const handleChange = React.useCallback((newValue: any) => {
+        const serializedValue = valueMode === 'json-string'
+            ? transformValue(newValue, fieldDef, valueMode, 'serialize')
+            : newValue;
+        field.onChange(serializedValue);
+    }, [field.onChange, fieldDef, valueMode]);
+
+    // Pre-define all change handlers at the top level to follow Rules of Hooks
+    const handleNumericChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+        const val = e.target.valueAsNumber;
+        handleChange(isNaN(val) ? (valueMode === 'json-string' ? '' : undefined) : val);
+    }, [handleChange, valueMode]);
+
+    const handleRegularNumericChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+        const val = e.target.valueAsNumber;
+        handleChange(isNaN(val) ? undefined : val);
+    }, [handleChange]);
+
+    const handleTextareaChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
+        handleChange(e.target.value);
+    }, [handleChange]);
+
+    const handleTextChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+        handleChange(e.target.value);
+    }, [handleChange]);
+
+    // Create props object for all renderers
+    const rendererProps: ComponentRendererProps = {
+        fieldDef,
+        field,
+        valueMode,
+        isReadonly,
+        transformedValue,
+        handleChange,
+        handleNumericChange,
+        handleRegularNumericChange,
+        handleTextareaChange,
+        handleTextChange,
+    };
+
+    // Try each renderer in order, return the first match
+    return (
+        renderRelationInput(rendererProps) ||
+        renderSelectInput(rendererProps) ||
+        renderNumericInput(rendererProps) ||
+        renderBooleanInput(rendererProps) ||
+        renderDateTimeInput(rendererProps) ||
+        renderTextareaInput(rendererProps) ||
+        renderTextInput(rendererProps)
+    );
+}

+ 143 - 0
packages/dashboard/src/lib/components/shared/value-transformers.ts

@@ -0,0 +1,143 @@
+import { UniversalFieldDefinition } from './universal-field-definition.js';
+
+export type ValueMode = 'native' | 'json-string';
+
+/**
+ * Interface for transforming values between native JavaScript types and JSON strings
+ */
+export interface ValueTransformer {
+    parse: (value: string, fieldDef: UniversalFieldDefinition) => any;
+    serialize: (value: any, fieldDef: UniversalFieldDefinition) => string;
+}
+
+/**
+ * Native value transformer - passes values through unchanged
+ */
+export const nativeValueTransformer: ValueTransformer = {
+    parse: (value: string, fieldDef: UniversalFieldDefinition) => {
+        // For native mode, values are already in their correct JavaScript type
+        return value;
+    },
+    serialize: (value: any, fieldDef: UniversalFieldDefinition) => {
+        // For native mode, values are already in their correct JavaScript type
+        return value;
+    },
+};
+
+/**
+ * JSON string value transformer - converts between JSON strings and native values
+ */
+export const jsonStringValueTransformer: ValueTransformer = {
+    parse: (value: string, fieldDef: UniversalFieldDefinition) => {
+        if (!value) {
+            return getDefaultValue(fieldDef);
+        }
+
+        try {
+            // For JSON string mode, parse the string to get the native value
+            const parsed = JSON.parse(value);
+
+            // Handle special cases for different field types
+            switch (fieldDef.type) {
+                case 'boolean':
+                    return parsed === true || parsed === 'true';
+                case 'int':
+                case 'float':
+                    return typeof parsed === 'number' ? parsed : parseFloat(parsed) || 0;
+                case 'datetime':
+                    return parsed;
+                default:
+                    return parsed;
+            }
+        } catch (error) {
+            // If parsing fails, try to handle as a plain string for certain types
+            switch (fieldDef.type) {
+                case 'boolean':
+                    return value === 'true';
+                case 'int':
+                case 'float':
+                    return parseFloat(value) || 0;
+                default:
+                    return value;
+            }
+        }
+    },
+    serialize: (value: any, fieldDef: UniversalFieldDefinition) => {
+        if (value === null || value === undefined) {
+            return '';
+        }
+
+        // Handle special cases for different field types
+        switch (fieldDef.type) {
+            case 'boolean':
+                return (value === true || value === 'true').toString();
+            case 'int':
+            case 'float':
+                return typeof value === 'number' ? value.toString() : (parseFloat(value) || 0).toString();
+            case 'string':
+                return typeof value === 'string' ? value : JSON.stringify(value);
+            default:
+                // For complex values (arrays, objects), serialize as JSON
+                return typeof value === 'string' ? value : JSON.stringify(value);
+        }
+    },
+};
+
+/**
+ * Get the appropriate value transformer based on the value mode
+ */
+export function getValueTransformer(valueMode: ValueMode): ValueTransformer {
+    switch (valueMode) {
+        case 'native':
+            return nativeValueTransformer;
+        case 'json-string':
+            return jsonStringValueTransformer;
+        default:
+            return nativeValueTransformer;
+    }
+}
+
+/**
+ * Get default value for a field type
+ */
+function getDefaultValue(fieldDef: UniversalFieldDefinition): any {
+    if (fieldDef.list) {
+        return [];
+    }
+
+    switch (fieldDef.type) {
+        case 'string':
+        case 'ID':
+        case 'localeString':
+        case 'localeText':
+            return '';
+        case 'int':
+        case 'float':
+            return 0;
+        case 'boolean':
+            return false;
+        case 'datetime':
+            return '';
+        case 'relation':
+            return fieldDef.list ? [] : null;
+        case 'struct':
+            return fieldDef.list ? [] : {};
+        default:
+            return '';
+    }
+}
+
+/**
+ * Utility to transform a value using the appropriate transformer
+ */
+export function transformValue(
+    value: any,
+    fieldDef: UniversalFieldDefinition,
+    valueMode: ValueMode,
+    direction: 'parse' | 'serialize',
+): any {
+    const transformer = getValueTransformer(valueMode);
+    return direction === 'parse'
+        ? transformer.parse(value, fieldDef)
+        : transformer.serialize(value, fieldDef);
+}

+ 4 - 4
packages/dashboard/src/lib/framework/form-engine/form-schema-tools.ts

@@ -136,12 +136,12 @@ function createStringValidationSchema(pattern?: string): ZodType {
  */
 function createIntValidationSchema(min?: number, max?: number): ZodType {
     let schema = z.number();
-    if (min !== undefined) {
+    if (min != null) {
         schema = schema.min(min, {
             message: `Value must be at least ${min}`,
         });
     }
-    if (max !== undefined) {
+    if (max != null) {
         schema = schema.max(max, {
             message: `Value must be at most ${max}`,
         });
@@ -159,12 +159,12 @@ function createIntValidationSchema(min?: number, max?: number): ZodType {
  */
 function createFloatValidationSchema(min?: number, max?: number): ZodType {
     let schema = z.number();
-    if (min !== undefined) {
+    if (min != null) {
         schema = schema.min(min, {
             message: `Value must be at least ${min}`,
         });
     }
-    if (max !== undefined) {
+    if (max != null) {
         schema = schema.max(max, {
             message: `Value must be at most ${max}`,
         });

+ 1 - 1
packages/dashboard/vite.config.mts

@@ -30,7 +30,7 @@ export default ({ mode }: { mode: string }) => {
         plugins: [
             vendureDashboardPlugin({
                 vendureConfigPath: pathToFileURL(vendureConfigPath),
-                adminUiConfig: { apiHost: adminApiHost, apiPort: adminApiPort },
+                api: { host: adminApiHost, port: adminApiPort },
                 gqlOutputPath: path.resolve(__dirname, './src/lib/graphql/'),
                 tempCompilationDir: path.resolve(__dirname, './.temp'),
             }) as any,

+ 620 - 0
packages/dev-server/test-plugins/field-test-plugin.ts

@@ -0,0 +1,620 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { Collection, PaymentMethodHandler, PluginCommonModule, Product, VendurePlugin } from '@vendure/core';
+
+/**
+ * @description
+ * A comprehensive test payment handler that exercises every type of configurable operation argument
+ * and UI component available in the dashboard. This handler is intended for development and testing
+ * purposes only to validate the universal form input system.
+ *
+ * Tests all DefaultFormComponentId values:
+ * - text-form-input, password-form-input, textarea-form-input
+ * - number-form-input, currency-form-input, boolean-form-input
+ * - select-form-input, date-form-input
+ * - rich-text-form-input, json-editor-form-input
+ *
+ * Tests all ConfigArgType values:
+ * - string, int, float, boolean, datetime, ID
+ * - Both single values and lists
+ * - Various UI configurations (min, max, step, options, etc.)
+ */
+const comprehensiveTestPaymentHandler = new PaymentMethodHandler({
+    code: 'comprehensive-test-payment-handler',
+    description: [
+        {
+            languageCode: LanguageCode.en,
+            value: 'Comprehensive test payment handler with all argument types and UI components',
+        },
+    ],
+    args: {
+        // === STRING ARGS ===
+        apiKey: {
+            type: 'string',
+            label: [{ languageCode: LanguageCode.en, value: 'API Key' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment gateway API key' }],
+            ui: { component: 'password-form-input' },
+            required: true,
+        },
+        merchantId: {
+            type: 'string',
+            label: [{ languageCode: LanguageCode.en, value: 'Merchant ID' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Merchant identifier' }],
+            ui: { component: 'text-form-input' },
+            required: true,
+        },
+        environment: {
+            type: 'string',
+            label: [{ languageCode: LanguageCode.en, value: 'Environment' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment environment' }],
+            ui: {
+                component: 'select-form-input',
+                options: [
+                    { value: 'sandbox', label: [{ languageCode: LanguageCode.en, value: 'Sandbox' }] },
+                    { value: 'production', label: [{ languageCode: LanguageCode.en, value: 'Production' }] },
+                ],
+            },
+            defaultValue: 'sandbox',
+        },
+        webhookUrl: {
+            type: 'string',
+            label: [{ languageCode: LanguageCode.en, value: 'Webhook URL' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Webhook endpoint URL' }],
+            ui: { component: 'textarea-form-input' },
+        },
+        // === STRING LIST ARGS ===
+        supportedCurrencies: {
+            type: 'string',
+            list: true,
+            label: [{ languageCode: LanguageCode.en, value: 'Supported Currencies' }],
+            description: [{ languageCode: LanguageCode.en, value: 'List of supported currency codes' }],
+        },
+        allowedCountries: {
+            type: 'string',
+            list: true,
+            label: [{ languageCode: LanguageCode.en, value: 'Allowed Countries' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Countries where payment is allowed' }],
+            ui: {
+                component: 'select-form-input',
+                options: [
+                    { value: 'US', label: [{ languageCode: LanguageCode.en, value: 'United States' }] },
+                    { value: 'GB', label: [{ languageCode: LanguageCode.en, value: 'United Kingdom' }] },
+                    { value: 'CA', label: [{ languageCode: LanguageCode.en, value: 'Canada' }] },
+                    { value: 'AU', label: [{ languageCode: LanguageCode.en, value: 'Australia' }] },
+                ],
+            },
+        },
+
+        // === INTEGER ARGS ===
+        timeout: {
+            type: 'int',
+            label: [{ languageCode: LanguageCode.en, value: 'Timeout (seconds)' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment request timeout' }],
+            ui: {
+                component: 'number-form-input',
+                min: 1,
+                max: 300,
+                step: 1,
+                suffix: 's',
+            },
+            defaultValue: 30,
+        },
+        maxRetries: {
+            type: 'int',
+            label: [{ languageCode: LanguageCode.en, value: 'Max Retries' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Maximum retry attempts' }],
+            ui: {
+                component: 'number-form-input',
+                min: 0,
+                max: 10,
+                step: 1,
+            },
+            defaultValue: 3,
+        },
+
+        // === FLOAT ARGS ===
+        processingFee: {
+            type: 'float',
+            label: [{ languageCode: LanguageCode.en, value: 'Processing Fee' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Processing fee percentage' }],
+            ui: {
+                component: 'number-form-input',
+                min: 0.0,
+                max: 10.0,
+                step: 0.01,
+                suffix: '%',
+            },
+            defaultValue: 2.5,
+        },
+        exchangeRate: {
+            type: 'float',
+            label: [{ languageCode: LanguageCode.en, value: 'Exchange Rate' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Currency exchange rate' }],
+            ui: {
+                component: 'number-form-input',
+                min: 0.01,
+                step: 0.0001,
+            },
+            defaultValue: 1.0,
+        },
+
+        // === BOOLEAN ARGS ===
+        enableLogging: {
+            type: 'boolean',
+            label: [{ languageCode: LanguageCode.en, value: 'Enable Logging' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Enable detailed logging' }],
+            ui: { component: 'boolean-form-input' },
+            defaultValue: false,
+        },
+        requireBillingAddress: {
+            type: 'boolean',
+            label: [{ languageCode: LanguageCode.en, value: 'Require Billing Address' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Require billing address for payments' }],
+            ui: { component: 'boolean-form-input' },
+            defaultValue: true,
+        },
+        testMode: {
+            type: 'boolean',
+            label: [{ languageCode: LanguageCode.en, value: 'Test Mode' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Enable test mode' }],
+            ui: { component: 'boolean-form-input' },
+            defaultValue: true,
+        },
+
+        // === DATETIME ARGS ===
+        validFrom: {
+            type: 'datetime',
+            label: [{ languageCode: LanguageCode.en, value: 'Valid From' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment method valid from date' }],
+            ui: { component: 'date-form-input' },
+        },
+        validUntil: {
+            type: 'datetime',
+            label: [{ languageCode: LanguageCode.en, value: 'Valid Until' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment method valid until date' }],
+            ui: { component: 'date-form-input' },
+        },
+
+        // === ID ARGS ===
+        partnerId: {
+            type: 'ID',
+            label: [{ languageCode: LanguageCode.en, value: 'Partner ID' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment partner identifier' }],
+        },
+        vendorId: {
+            type: 'ID',
+            label: [{ languageCode: LanguageCode.en, value: 'Vendor ID' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment vendor identifier' }],
+        },
+
+        // === SPECIALIZED UI COMPONENTS ===
+        baseCurrency: {
+            type: 'string',
+            label: [{ languageCode: LanguageCode.en, value: 'Base Currency' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Base currency for calculations' }],
+            ui: { component: 'currency-form-input' },
+            defaultValue: 'USD',
+        },
+
+        termsAndConditions: {
+            type: 'string',
+            label: [{ languageCode: LanguageCode.en, value: 'Terms and Conditions' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Payment terms and conditions' }],
+            ui: { component: 'rich-text-form-input' },
+        },
+
+        advancedConfig: {
+            type: 'string',
+            label: [{ languageCode: LanguageCode.en, value: 'Advanced Configuration' }],
+            description: [{ languageCode: LanguageCode.en, value: 'Advanced JSON configuration' }],
+            ui: {
+                component: 'json-editor-form-input',
+                height: '200px',
+            },
+            defaultValue: '{"webhookRetries": 3, "timeout": 30000}',
+        },
+    },
+    createPayment: async (ctx, order, amount, args, metadata) => {
+        // Simulate different payment outcomes based on metadata
+        if (metadata.shouldDecline) {
+            return {
+                amount,
+                state: 'Declined' as const,
+                metadata: {
+                    errorMessage: 'Test decline simulation',
+                },
+            };
+        } else if (metadata.shouldError) {
+            return {
+                amount,
+                state: 'Error' as const,
+                errorMessage: 'Test error simulation',
+                metadata: {
+                    errorMessage: 'Test error simulation',
+                },
+            };
+        } else {
+            return {
+                amount,
+                state: args.testMode ? 'Authorized' : 'Settled',
+                transactionId: 'test-' + Math.random().toString(36).substr(2, 9),
+                metadata: {
+                    ...metadata,
+                    processingFee: args.processingFee,
+                    environment: args.environment,
+                },
+            };
+        }
+    },
+    settlePayment: async (ctx, order, payment, args) => {
+        if (payment.metadata.shouldErrorOnSettle) {
+            return {
+                success: false,
+                errorMessage: 'Test settlement error simulation',
+            };
+        }
+        return {
+            success: true,
+            metadata: {
+                settledAt: new Date().toISOString(),
+                processingFee: args.processingFee,
+            },
+        };
+    },
+    cancelPayment: async (ctx, order, payment) => {
+        return {
+            success: true,
+            metadata: {
+                cancellationDate: new Date().toISOString(),
+                reason: 'Test cancellation',
+            },
+        };
+    },
+});
+
+/**
+ * @description
+ * FieldTestPlugin provides comprehensive test cases for all custom field types and
+ * configurable operation argument types supported by Vendure. This plugin is designed
+ * specifically for development and testing purposes to validate the universal form
+ * input system in the dashboard.
+ *
+ * ## Custom Fields Coverage
+ * Tests all CustomFieldType values on the Product entity:
+ * - string (with and without options, lists)
+ * - localeString (translatable strings)
+ * - text (long text fields)
+ * - localeText (translatable long text)
+ * - int (with min/max/step validation)
+ * - float (with precision controls)
+ * - boolean (single and list)
+ * - datetime (dates and date lists)
+ * - relation (single and multi-relation)
+ * - struct (complex objects and lists)
+ *
+ * ## Configurable Operation Args Coverage
+ * Tests all ConfigArgType values and DefaultFormComponentId components:
+ * - All basic types: string, int, float, boolean, datetime, ID
+ * - All UI components: text, password, textarea, number, currency, boolean,
+ *   select, date, rich-text, json-editor
+ * - Advanced features: lists, options, validation, prefixes/suffixes
+ *
+ * ## UI Features Tested
+ * - Tab organization
+ * - Full-width layouts
+ * - Readonly fields
+ * - Field validation (min/max/step)
+ * - Select options and multi-select
+ * - List field management
+ * - Custom UI component integration
+ *
+ * ## Usage
+ * 1. Add this plugin to your dev-config.ts plugins array
+ * 2. Navigate to any Product detail page to see custom fields
+ * 3. Go to Settings → Payment Methods → Add "Comprehensive Test Payment Handler"
+ *    to see configurable operation arguments
+ *
+ * @docsCategory plugin
+ * @since 3.4.0
+ */
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    configuration: config => {
+        // Add comprehensive custom fields to Product entity
+        config.customFields.Product.push(
+            // === STRING FIELDS ===
+            {
+                name: 'infoUrl',
+                type: 'string',
+                label: [{ languageCode: LanguageCode.en, value: 'Info URL' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product information URL' }],
+            },
+            {
+                name: 'customSku',
+                type: 'string',
+                label: [{ languageCode: LanguageCode.en, value: 'Custom SKU' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Custom SKU for this product' }],
+                readonly: true,
+            },
+            {
+                name: 'category',
+                type: 'string',
+                list: false,
+                label: [{ languageCode: LanguageCode.en, value: 'Category' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product category selection' }],
+                options: [
+                    {
+                        value: 'electronics',
+                        label: [{ languageCode: LanguageCode.en, value: 'Electronics' }],
+                    },
+                    { value: 'clothing', label: [{ languageCode: LanguageCode.en, value: 'Clothing' }] },
+                    { value: 'books', label: [{ languageCode: LanguageCode.en, value: 'Books' }] },
+                    { value: 'home', label: [{ languageCode: LanguageCode.en, value: 'Home & Garden' }] },
+                ],
+            },
+            {
+                name: 'tags',
+                type: 'string',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Tags' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product tags (list)' }],
+            },
+            {
+                name: 'features',
+                type: 'string',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Key Features' }],
+                description: [{ languageCode: LanguageCode.en, value: 'List of product features' }],
+                options: [
+                    { value: 'wireless', label: [{ languageCode: LanguageCode.en, value: 'Wireless' }] },
+                    { value: 'waterproof', label: [{ languageCode: LanguageCode.en, value: 'Waterproof' }] },
+                    {
+                        value: 'rechargeable',
+                        label: [{ languageCode: LanguageCode.en, value: 'Rechargeable' }],
+                    },
+                    { value: 'portable', label: [{ languageCode: LanguageCode.en, value: 'Portable' }] },
+                ],
+            },
+
+            // === LOCALE STRING FIELDS ===
+            {
+                name: 'shortName',
+                type: 'localeString',
+                label: [{ languageCode: LanguageCode.en, value: 'Short Name' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Short product name (translatable)' }],
+            },
+            {
+                name: 'seoTitle',
+                type: 'localeString',
+                label: [{ languageCode: LanguageCode.en, value: 'SEO Title' }],
+                description: [{ languageCode: LanguageCode.en, value: 'SEO page title (translatable)' }],
+                ui: { tab: 'SEO' },
+            },
+
+            // === TEXT FIELDS ===
+            {
+                name: 'specifications',
+                type: 'text',
+                label: [{ languageCode: LanguageCode.en, value: 'Specifications' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product specifications (long text)' }],
+                ui: { fullWidth: true },
+            },
+            {
+                name: 'warrantyInfo',
+                type: 'localeText',
+                label: [{ languageCode: LanguageCode.en, value: 'Warranty Information' }],
+                description: [
+                    { languageCode: LanguageCode.en, value: 'Warranty details (translatable long text)' },
+                ],
+                ui: { fullWidth: true, tab: 'Details' },
+            },
+
+            // === BOOLEAN FIELDS ===
+            {
+                name: 'downloadable',
+                type: 'boolean',
+                label: [{ languageCode: LanguageCode.en, value: 'Downloadable' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Is this a downloadable product?' }],
+            },
+            {
+                name: 'featured',
+                type: 'boolean',
+                label: [{ languageCode: LanguageCode.en, value: 'Featured Product' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Show on homepage' }],
+            },
+            {
+                name: 'exclusiveOffers',
+                type: 'boolean',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Exclusive Offers' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Multiple boolean values' }],
+            },
+
+            // === INTEGER FIELDS ===
+            {
+                name: 'weight',
+                type: 'int',
+                label: [{ languageCode: LanguageCode.en, value: 'Weight (grams)' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product weight in grams' }],
+                min: 0,
+                max: 50000,
+                step: 10,
+            },
+            {
+                name: 'priority',
+                type: 'int',
+                label: [{ languageCode: LanguageCode.en, value: 'Priority' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Display priority (1-10)' }],
+                min: 1,
+                max: 10,
+                step: 1,
+            },
+            {
+                name: 'dimensions',
+                type: 'int',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Dimensions (L×W×H)' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product dimensions in cm' }],
+                min: 0,
+                max: 1000,
+            },
+
+            // === FLOAT FIELDS ===
+            {
+                name: 'rating',
+                type: 'float',
+                label: [{ languageCode: LanguageCode.en, value: 'Average Rating' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Average customer rating' }],
+                min: 0.0,
+                max: 5.0,
+                step: 0.1,
+                readonly: true,
+            },
+            {
+                name: 'temperature',
+                type: 'float',
+                label: [{ languageCode: LanguageCode.en, value: 'Operating Temperature' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Operating temperature range' }],
+                min: -40.0,
+                max: 85.0,
+                step: 0.5,
+            },
+            {
+                name: 'measurements',
+                type: 'float',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Measurements' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Precise measurements list' }],
+                step: 0.01,
+            },
+
+            // === DATETIME FIELDS ===
+            {
+                name: 'lastUpdated',
+                type: 'datetime',
+                label: [{ languageCode: LanguageCode.en, value: 'Last Updated' }],
+                description: [{ languageCode: LanguageCode.en, value: 'When product was last updated' }],
+                readonly: true,
+            },
+            {
+                name: 'releaseDate',
+                type: 'datetime',
+                label: [{ languageCode: LanguageCode.en, value: 'Release Date' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product release date' }],
+            },
+            {
+                name: 'availabilityDates',
+                type: 'datetime',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Availability Dates' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Special availability dates' }],
+            },
+
+            // === RELATION FIELDS ===
+            {
+                name: 'brand',
+                type: 'relation',
+                entity: Collection,
+                label: [{ languageCode: LanguageCode.en, value: 'Brand' }],
+                description: [
+                    { languageCode: LanguageCode.en, value: 'Product brand (collection relation)' },
+                ],
+            },
+            {
+                name: 'relatedProducts',
+                type: 'relation',
+                entity: Product,
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Related Products' }],
+                description: [{ languageCode: LanguageCode.en, value: 'List of related products' }],
+            },
+            {
+                name: 'manufacturer',
+                type: 'relation',
+                entity: Collection,
+                label: [{ languageCode: LanguageCode.en, value: 'Manufacturer' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Product manufacturer' }],
+                ui: { tab: 'Details' },
+            },
+
+            // === STRUCT FIELDS ===
+            {
+                name: 'productSpecs',
+                type: 'struct',
+                label: [{ languageCode: LanguageCode.en, value: 'Product Specifications' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Structured product specifications' }],
+                ui: { fullWidth: true, tab: 'Specifications' },
+                fields: [
+                    { name: 'cpu', type: 'string' as const },
+                    { name: 'memory', type: 'int' as const },
+                    { name: 'storage', type: 'int' as const },
+                    { name: 'display', type: 'string' as const },
+                ],
+            },
+            {
+                name: 'variations',
+                type: 'struct',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'Product Variations' }],
+                description: [
+                    { languageCode: LanguageCode.en, value: 'List of product variant specifications' },
+                ],
+                ui: { fullWidth: true, tab: 'Variants' },
+                fields: [
+                    { name: 'color', type: 'string' as const },
+                    { name: 'size', type: 'string' as const },
+                    { name: 'price', type: 'float' as const },
+                    { name: 'inStock', type: 'boolean' as const },
+                ],
+            },
+
+            // === FIELDS WITH CUSTOM UI COMPONENTS (if available) ===
+            {
+                name: 'customData',
+                type: 'string',
+                label: [{ languageCode: LanguageCode.en, value: 'Custom Data' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Field with custom UI component' }],
+                ui: {
+                    component: 'custom-text-input', // This would need to be registered
+                    tab: 'Advanced',
+                },
+            },
+
+            // === FIELDS WITH TABS ===
+            {
+                name: 'seoDescription',
+                type: 'text',
+                label: [{ languageCode: LanguageCode.en, value: 'SEO Description' }],
+                description: [{ languageCode: LanguageCode.en, value: 'SEO meta description' }],
+                ui: { tab: 'SEO', fullWidth: true },
+            },
+            {
+                name: 'seoKeywords',
+                type: 'string',
+                list: true,
+                label: [{ languageCode: LanguageCode.en, value: 'SEO Keywords' }],
+                description: [{ languageCode: LanguageCode.en, value: 'SEO keywords' }],
+                ui: { tab: 'SEO' },
+            },
+            {
+                name: 'technicalNotes',
+                type: 'text',
+                label: [{ languageCode: LanguageCode.en, value: 'Technical Notes' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Internal technical notes' }],
+                ui: { tab: 'Internal', fullWidth: true },
+            },
+            {
+                name: 'internalCode',
+                type: 'string',
+                label: [{ languageCode: LanguageCode.en, value: 'Internal Code' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Internal tracking code' }],
+                ui: { tab: 'Internal' },
+            },
+        );
+
+        // Add comprehensive test payment handler
+        config.paymentOptions.paymentMethodHandlers.push(comprehensiveTestPaymentHandler);
+
+        return config;
+    },
+})
+export class FieldTestPlugin {}