@vendure/dashboard 3.3.8-master-202507260236 → 3.3.8-master-202507300243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +11 -78
- package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +11 -81
- package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +10 -77
- package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +12 -87
- package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +12 -87
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +10 -80
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +10 -79
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +8 -6
- package/src/lib/components/data-input/combination-mode-input.tsx +52 -0
- package/src/lib/components/data-input/configurable-operation-list-input.tsx +433 -0
- package/src/lib/components/data-input/custom-field-list-input.tsx +297 -0
- package/src/lib/components/data-input/datetime-input.tsx +5 -2
- package/src/lib/components/data-input/default-relation-input.tsx +599 -0
- package/src/lib/components/data-input/index.ts +6 -0
- package/src/lib/components/data-input/product-multi-selector.tsx +426 -0
- package/src/lib/components/data-input/relation-selector.tsx +7 -6
- package/src/lib/components/data-input/select-with-options.tsx +84 -0
- package/src/lib/components/data-input/struct-form-input.tsx +324 -0
- package/src/lib/components/shared/configurable-operation-arg-input.tsx +365 -21
- package/src/lib/components/shared/configurable-operation-input.tsx +81 -41
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +260 -0
- package/src/lib/components/shared/configurable-operation-selector.tsx +156 -0
- package/src/lib/components/shared/custom-fields-form.tsx +207 -36
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/components/ui/form.tsx +4 -4
- package/src/lib/framework/extension-api/input-component-extensions.tsx +5 -1
- package/src/lib/framework/form-engine/form-schema-tools.spec.ts +472 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +340 -5
- package/src/lib/framework/form-engine/use-generated-form.tsx +24 -8
- package/src/lib/framework/form-engine/utils.ts +3 -9
- package/src/lib/framework/layout-engine/page-layout.tsx +11 -3
- package/src/lib/framework/page/use-detail-page.ts +3 -3
- package/src/lib/lib/utils.ts +26 -24
|
@@ -1,21 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
DropdownMenu,
|
|
5
|
-
DropdownMenuContent,
|
|
6
|
-
DropdownMenuItem,
|
|
7
|
-
DropdownMenuTrigger,
|
|
8
|
-
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
9
|
-
import { api } from '@/vdb/graphql/api.js';
|
|
10
|
-
import {
|
|
11
|
-
configurableOperationDefFragment,
|
|
12
|
-
ConfigurableOperationDefFragment,
|
|
13
|
-
} from '@/vdb/graphql/fragments.js';
|
|
1
|
+
import { ConfigurableOperationSelector } from '@/vdb/components/shared/configurable-operation-selector.js';
|
|
2
|
+
import { configurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
|
|
14
3
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
15
|
-
import { Trans } from '@/vdb/lib/trans.js';
|
|
16
|
-
import { useQuery } from '@tanstack/react-query';
|
|
17
4
|
import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
|
|
18
|
-
import { Plus } from 'lucide-react';
|
|
19
5
|
|
|
20
6
|
export const shippingEligibilityCheckersDocument = graphql(
|
|
21
7
|
`
|
|
@@ -37,69 +23,14 @@ export function ShippingEligibilityCheckerSelector({
|
|
|
37
23
|
value,
|
|
38
24
|
onChange,
|
|
39
25
|
}: ShippingEligibilityCheckerSelectorProps) {
|
|
40
|
-
const { data: checkersData } = useQuery({
|
|
41
|
-
queryKey: ['shippingEligibilityCheckers'],
|
|
42
|
-
queryFn: () => api.query(shippingEligibilityCheckersDocument),
|
|
43
|
-
staleTime: 1000 * 60 * 60 * 5,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const checkers = checkersData?.shippingEligibilityCheckers;
|
|
47
|
-
|
|
48
|
-
const onCheckerSelected = (checker: ConfigurableOperationDefFragment) => {
|
|
49
|
-
const checkerDef = checkers?.find(c => c.code === checker.code);
|
|
50
|
-
if (!checkerDef) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
onChange({
|
|
54
|
-
code: checker.code,
|
|
55
|
-
arguments: checkerDef.args.map(arg => ({
|
|
56
|
-
name: arg.name,
|
|
57
|
-
value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
|
|
58
|
-
})),
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const onOperationValueChange = (newVal: ConfigurableOperationInputType) => {
|
|
63
|
-
onChange(newVal);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const onOperationRemove = () => {
|
|
67
|
-
onChange(undefined);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const checkerDef = checkers?.find(c => c.code === value?.code);
|
|
71
|
-
|
|
72
26
|
return (
|
|
73
|
-
<
|
|
74
|
-
{value
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/>
|
|
82
|
-
</div>
|
|
83
|
-
)}
|
|
84
|
-
<DropdownMenu>
|
|
85
|
-
{!value && (
|
|
86
|
-
<DropdownMenuTrigger asChild>
|
|
87
|
-
<Button variant="outline">
|
|
88
|
-
<Plus />
|
|
89
|
-
<Trans context="Add new promotion action">
|
|
90
|
-
Select Shipping Eligibility Checker
|
|
91
|
-
</Trans>
|
|
92
|
-
</Button>
|
|
93
|
-
</DropdownMenuTrigger>
|
|
94
|
-
)}
|
|
95
|
-
<DropdownMenuContent className="w-96">
|
|
96
|
-
{checkers?.map(checker => (
|
|
97
|
-
<DropdownMenuItem key={checker.code} onClick={() => onCheckerSelected(checker)}>
|
|
98
|
-
{checker.description}
|
|
99
|
-
</DropdownMenuItem>
|
|
100
|
-
))}
|
|
101
|
-
</DropdownMenuContent>
|
|
102
|
-
</DropdownMenu>
|
|
103
|
-
</div>
|
|
27
|
+
<ConfigurableOperationSelector
|
|
28
|
+
value={value}
|
|
29
|
+
onChange={onChange}
|
|
30
|
+
queryDocument={shippingEligibilityCheckersDocument}
|
|
31
|
+
queryKey="shippingEligibilityCheckers"
|
|
32
|
+
dataPath="shippingEligibilityCheckers"
|
|
33
|
+
buttonText="Select Shipping Eligibility Checker"
|
|
34
|
+
/>
|
|
104
35
|
);
|
|
105
36
|
}
|
|
@@ -130,12 +130,14 @@ function ShippingMethodDetailPage() {
|
|
|
130
130
|
render={({ field }) => <Input {...field} />}
|
|
131
131
|
/>
|
|
132
132
|
</DetailFormGrid>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
<div className="mb-6">
|
|
134
|
+
<TranslatableFormFieldWrapper
|
|
135
|
+
control={form.control}
|
|
136
|
+
name="description"
|
|
137
|
+
label={<Trans>Description</Trans>}
|
|
138
|
+
render={({ field }) => <Textarea {...field} />}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
139
141
|
<DetailFormGrid>
|
|
140
142
|
<FormFieldWrapper
|
|
141
143
|
control={form.control}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DataInputComponent } from '@/vdb/framework/component-registry/component-registry.js';
|
|
2
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
3
|
+
|
|
4
|
+
export const CombinationModeInput: DataInputComponent = ({ value, onChange, position, ...props }) => {
|
|
5
|
+
const booleanValue = value === 'true' || value === true;
|
|
6
|
+
|
|
7
|
+
// Only show for items after the first one
|
|
8
|
+
const selectable = position !== undefined && position > 0;
|
|
9
|
+
|
|
10
|
+
const setCombinationModeAnd = () => {
|
|
11
|
+
onChange(true);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const setCombinationModeOr = () => {
|
|
15
|
+
onChange(false);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (!selectable) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex items-center justify-center -mt-4 -mb-4">
|
|
24
|
+
<div className="bg-muted border px-3 py-1.5 rounded-full flex gap-1.5 text-xs shadow-sm">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
className={`px-2 py-0.5 rounded-full transition-colors ${
|
|
28
|
+
booleanValue
|
|
29
|
+
? 'bg-primary text-background'
|
|
30
|
+
: 'text-muted-foreground hover:bg-muted-foreground/10'
|
|
31
|
+
}`}
|
|
32
|
+
onClick={setCombinationModeAnd}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<Trans>AND</Trans>
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className={`px-2 py-0.5 rounded-full transition-colors ${
|
|
40
|
+
!booleanValue
|
|
41
|
+
? 'bg-primary text-background'
|
|
42
|
+
: 'text-muted-foreground hover:bg-muted-foreground/10'
|
|
43
|
+
}`}
|
|
44
|
+
onClick={setCombinationModeOr}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
<Trans>OR</Trans>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { ConfigurableOperationDefFragment } from '@/vdb/graphql/fragments.js';
|
|
2
|
+
import { ConfigArgType } from '@vendure/core';
|
|
3
|
+
import { Plus, X } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { Button } from '../ui/button.js';
|
|
6
|
+
import { Input } from '../ui/input.js';
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
|
|
8
|
+
import { Switch } from '../ui/switch.js';
|
|
9
|
+
import { Textarea } from '../ui/textarea.js';
|
|
10
|
+
import { DateTimeInput } from './datetime-input.js';
|
|
11
|
+
|
|
12
|
+
export interface EnhancedListInputProps {
|
|
13
|
+
definition: ConfigurableOperationDefFragment['args'][number];
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A dynamic array input component for configurable operation arguments that handle lists of values.
|
|
21
|
+
*
|
|
22
|
+
* This component allows users to add, edit, and remove multiple items from an array-type argument.
|
|
23
|
+
* Each item in the array is rendered using the appropriate input control based on the argument's
|
|
24
|
+
* type and UI configuration (e.g., text input, select dropdown, boolean switch, date picker).
|
|
25
|
+
*
|
|
26
|
+
* The component supports:
|
|
27
|
+
* - Adding new items with appropriate input controls
|
|
28
|
+
* - Editing existing items inline
|
|
29
|
+
* - Removing items from the array
|
|
30
|
+
* - Various data types: string, number, boolean, datetime, currency
|
|
31
|
+
* - Multiple UI components: select, textarea, currency input, etc.
|
|
32
|
+
* - Keyboard shortcuts (Enter to add items)
|
|
33
|
+
* - Read-only mode for display purposes
|
|
34
|
+
*
|
|
35
|
+
* Used primarily in configurable operations (promotions, shipping methods, payment methods)
|
|
36
|
+
* where an argument accepts multiple values, such as a list of product IDs, category codes,
|
|
37
|
+
* or discount amounts.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // For a promotion condition that accepts multiple product category codes
|
|
41
|
+
* <EnhancedListInput
|
|
42
|
+
* definition={argDefinition}
|
|
43
|
+
* value='["electronics", "books", "clothing"]'
|
|
44
|
+
* onChange={handleChange}
|
|
45
|
+
* />
|
|
46
|
+
*/
|
|
47
|
+
export function ConfigurableOperationListInput({
|
|
48
|
+
definition,
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
readOnly,
|
|
52
|
+
}: Readonly<EnhancedListInputProps>) {
|
|
53
|
+
const [newItemValue, setNewItemValue] = useState('');
|
|
54
|
+
|
|
55
|
+
// Parse the current array value
|
|
56
|
+
const arrayValue = parseArrayValue(value);
|
|
57
|
+
|
|
58
|
+
const handleArrayChange = (newArray: string[]) => {
|
|
59
|
+
onChange(JSON.stringify(newArray));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleAddItem = () => {
|
|
63
|
+
if (newItemValue.trim()) {
|
|
64
|
+
const newArray = [...arrayValue, newItemValue.trim()];
|
|
65
|
+
handleArrayChange(newArray);
|
|
66
|
+
setNewItemValue('');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleRemoveItem = (index: number) => {
|
|
71
|
+
const newArray = arrayValue.filter((_, i) => i !== index);
|
|
72
|
+
handleArrayChange(newArray);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleUpdateItem = (index: number, newValue: string) => {
|
|
76
|
+
const newArray = arrayValue.map((item, i) => (i === index ? newValue : item));
|
|
77
|
+
handleArrayChange(newArray);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
81
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
handleAddItem();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Render individual item input based on the underlying type
|
|
88
|
+
const renderItemInput = (itemValue: string, index: number) => {
|
|
89
|
+
const argType = definition.type as ConfigArgType;
|
|
90
|
+
const uiComponent = (definition.ui as any)?.component;
|
|
91
|
+
|
|
92
|
+
const commonProps = {
|
|
93
|
+
value: itemValue,
|
|
94
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
|
95
|
+
handleUpdateItem(index, e.target.value),
|
|
96
|
+
disabled: readOnly,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
switch (uiComponent) {
|
|
100
|
+
case 'boolean-form-input':
|
|
101
|
+
return (
|
|
102
|
+
<Switch
|
|
103
|
+
checked={itemValue === 'true'}
|
|
104
|
+
onCheckedChange={checked => handleUpdateItem(index, checked.toString())}
|
|
105
|
+
disabled={readOnly}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
case 'select-form-input': {
|
|
110
|
+
const options = (definition.ui as any)?.options || [];
|
|
111
|
+
return (
|
|
112
|
+
<Select
|
|
113
|
+
value={itemValue}
|
|
114
|
+
onValueChange={val => handleUpdateItem(index, val)}
|
|
115
|
+
disabled={readOnly}
|
|
116
|
+
>
|
|
117
|
+
<SelectTrigger>
|
|
118
|
+
<SelectValue />
|
|
119
|
+
</SelectTrigger>
|
|
120
|
+
<SelectContent>
|
|
121
|
+
{options.map((option: any) => (
|
|
122
|
+
<SelectItem key={option.value} value={option.value}>
|
|
123
|
+
{typeof option.label === 'string'
|
|
124
|
+
? option.label
|
|
125
|
+
: option.label?.[0]?.value || option.value}
|
|
126
|
+
</SelectItem>
|
|
127
|
+
))}
|
|
128
|
+
</SelectContent>
|
|
129
|
+
</Select>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
case 'textarea-form-input':
|
|
133
|
+
return (
|
|
134
|
+
<Textarea
|
|
135
|
+
{...commonProps}
|
|
136
|
+
placeholder="Enter text..."
|
|
137
|
+
rows={2}
|
|
138
|
+
className="bg-background"
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
case 'date-form-input':
|
|
143
|
+
return (
|
|
144
|
+
<DateTimeInput
|
|
145
|
+
value={itemValue ? new Date(itemValue) : new Date()}
|
|
146
|
+
onChange={val => handleUpdateItem(index, val.toISOString())}
|
|
147
|
+
disabled={readOnly}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
case 'number-form-input': {
|
|
152
|
+
const ui = definition.ui as any;
|
|
153
|
+
const isFloat = argType === 'float';
|
|
154
|
+
return (
|
|
155
|
+
<Input
|
|
156
|
+
type="number"
|
|
157
|
+
value={itemValue}
|
|
158
|
+
onChange={e => handleUpdateItem(index, e.target.value)}
|
|
159
|
+
disabled={readOnly}
|
|
160
|
+
min={ui?.min}
|
|
161
|
+
max={ui?.max}
|
|
162
|
+
step={ui?.step || (isFloat ? 0.01 : 1)}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
case 'currency-form-input':
|
|
167
|
+
return (
|
|
168
|
+
<div className="flex items-center">
|
|
169
|
+
<span className="mr-2 text-sm text-muted-foreground">$</span>
|
|
170
|
+
<Input
|
|
171
|
+
type="number"
|
|
172
|
+
value={itemValue}
|
|
173
|
+
onChange={e => handleUpdateItem(index, e.target.value)}
|
|
174
|
+
disabled={readOnly}
|
|
175
|
+
min={0}
|
|
176
|
+
step={1}
|
|
177
|
+
className="flex-1"
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fall back to type-based rendering
|
|
184
|
+
switch (argType) {
|
|
185
|
+
case 'boolean':
|
|
186
|
+
return (
|
|
187
|
+
<Switch
|
|
188
|
+
checked={itemValue === 'true'}
|
|
189
|
+
onCheckedChange={checked => handleUpdateItem(index, checked.toString())}
|
|
190
|
+
disabled={readOnly}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
case 'int':
|
|
195
|
+
case 'float': {
|
|
196
|
+
const isFloat = argType === 'float';
|
|
197
|
+
return (
|
|
198
|
+
<Input
|
|
199
|
+
type="number"
|
|
200
|
+
value={itemValue}
|
|
201
|
+
onChange={e => handleUpdateItem(index, e.target.value)}
|
|
202
|
+
disabled={readOnly}
|
|
203
|
+
step={isFloat ? 0.01 : 1}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
case 'datetime':
|
|
208
|
+
return (
|
|
209
|
+
<DateTimeInput
|
|
210
|
+
value={itemValue ? new Date(itemValue) : new Date()}
|
|
211
|
+
onChange={val => handleUpdateItem(index, val.toISOString())}
|
|
212
|
+
disabled={readOnly}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
default:
|
|
217
|
+
return <Input type="text" {...commonProps} placeholder="Enter value..." />;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Render new item input (similar logic but for newItemValue)
|
|
222
|
+
const renderNewItemInput = () => {
|
|
223
|
+
const argType = definition.type as ConfigArgType;
|
|
224
|
+
const uiComponent = (definition.ui as any)?.component;
|
|
225
|
+
|
|
226
|
+
const commonProps = {
|
|
227
|
+
value: newItemValue,
|
|
228
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
|
229
|
+
setNewItemValue(e.target.value),
|
|
230
|
+
disabled: readOnly,
|
|
231
|
+
onKeyPress: handleKeyPress,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
switch (uiComponent) {
|
|
235
|
+
case 'boolean-form-input': {
|
|
236
|
+
return (
|
|
237
|
+
<Switch
|
|
238
|
+
checked={newItemValue === 'true'}
|
|
239
|
+
onCheckedChange={checked => setNewItemValue(checked.toString())}
|
|
240
|
+
disabled={readOnly}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
case 'select-form-input': {
|
|
245
|
+
const options = (definition.ui as any)?.options || [];
|
|
246
|
+
return (
|
|
247
|
+
<Select value={newItemValue} onValueChange={setNewItemValue} disabled={readOnly}>
|
|
248
|
+
<SelectTrigger>
|
|
249
|
+
<SelectValue placeholder="Select value..." />
|
|
250
|
+
</SelectTrigger>
|
|
251
|
+
<SelectContent>
|
|
252
|
+
{options.map((option: any) => (
|
|
253
|
+
<SelectItem key={option.value} value={option.value}>
|
|
254
|
+
{typeof option.label === 'string'
|
|
255
|
+
? option.label
|
|
256
|
+
: option.label?.[0]?.value || option.value}
|
|
257
|
+
</SelectItem>
|
|
258
|
+
))}
|
|
259
|
+
</SelectContent>
|
|
260
|
+
</Select>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
case 'textarea-form-input': {
|
|
264
|
+
return (
|
|
265
|
+
<Textarea
|
|
266
|
+
{...commonProps}
|
|
267
|
+
placeholder="Enter text..."
|
|
268
|
+
rows={2}
|
|
269
|
+
className="bg-background"
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
case 'date-form-input': {
|
|
274
|
+
return <DateTimeInput value={newItemValue} onChange={setNewItemValue} disabled={readOnly} />;
|
|
275
|
+
}
|
|
276
|
+
case 'number-form-input': {
|
|
277
|
+
const ui = definition.ui as any;
|
|
278
|
+
const isFloat = argType === 'float';
|
|
279
|
+
return (
|
|
280
|
+
<Input
|
|
281
|
+
type="number"
|
|
282
|
+
value={newItemValue}
|
|
283
|
+
onChange={e => setNewItemValue(e.target.value)}
|
|
284
|
+
disabled={readOnly}
|
|
285
|
+
min={ui?.min}
|
|
286
|
+
max={ui?.max}
|
|
287
|
+
step={ui?.step || (isFloat ? 0.01 : 1)}
|
|
288
|
+
placeholder="Enter number..."
|
|
289
|
+
onKeyPress={handleKeyPress}
|
|
290
|
+
className="bg-background"
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
case 'currency-form-input': {
|
|
295
|
+
return (
|
|
296
|
+
<div className="flex items-center">
|
|
297
|
+
<span className="mr-2 text-sm text-muted-foreground">$</span>
|
|
298
|
+
<Input
|
|
299
|
+
type="number"
|
|
300
|
+
value={newItemValue}
|
|
301
|
+
onChange={e => setNewItemValue(e.target.value)}
|
|
302
|
+
disabled={readOnly}
|
|
303
|
+
min={0}
|
|
304
|
+
step={1}
|
|
305
|
+
placeholder="Enter amount..."
|
|
306
|
+
onKeyPress={handleKeyPress}
|
|
307
|
+
className="flex-1 bg-background"
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Fall back to type-based rendering
|
|
315
|
+
switch (argType) {
|
|
316
|
+
case 'boolean':
|
|
317
|
+
return (
|
|
318
|
+
<Switch
|
|
319
|
+
checked={newItemValue === 'true'}
|
|
320
|
+
onCheckedChange={checked => setNewItemValue(checked.toString())}
|
|
321
|
+
disabled={readOnly}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
case 'int':
|
|
325
|
+
case 'float': {
|
|
326
|
+
const isFloat = argType === 'float';
|
|
327
|
+
return (
|
|
328
|
+
<Input
|
|
329
|
+
type="number"
|
|
330
|
+
value={newItemValue}
|
|
331
|
+
onChange={e => setNewItemValue(e.target.value)}
|
|
332
|
+
disabled={readOnly}
|
|
333
|
+
step={isFloat ? 0.01 : 1}
|
|
334
|
+
placeholder="Enter number..."
|
|
335
|
+
onKeyPress={handleKeyPress}
|
|
336
|
+
className="bg-background"
|
|
337
|
+
/>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
case 'datetime': {
|
|
341
|
+
return (
|
|
342
|
+
<DateTimeInput
|
|
343
|
+
value={newItemValue ? new Date(newItemValue) : new Date()}
|
|
344
|
+
onChange={val => setNewItemValue(val.toISOString())}
|
|
345
|
+
disabled={readOnly}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
default: {
|
|
350
|
+
return (
|
|
351
|
+
<Input
|
|
352
|
+
type="text"
|
|
353
|
+
{...commonProps}
|
|
354
|
+
placeholder="Enter value..."
|
|
355
|
+
className="bg-background"
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
if (readOnly) {
|
|
363
|
+
return (
|
|
364
|
+
<div className="space-y-2">
|
|
365
|
+
{arrayValue.map((item, index) => (
|
|
366
|
+
<div key={index + item} className="flex items-center gap-2 p-2 bg-muted rounded-md">
|
|
367
|
+
<span className="flex-1">{item}</span>
|
|
368
|
+
</div>
|
|
369
|
+
))}
|
|
370
|
+
{arrayValue.length === 0 && <div className="text-sm text-muted-foreground">No items</div>}
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<div className="space-y-2">
|
|
377
|
+
{/* Existing items */}
|
|
378
|
+
{arrayValue.map((item, index) => (
|
|
379
|
+
<div key={index + item} className="flex items-center gap-2">
|
|
380
|
+
<div className="flex-1">{renderItemInput(item, index)}</div>
|
|
381
|
+
<Button
|
|
382
|
+
variant="outline"
|
|
383
|
+
size="sm"
|
|
384
|
+
onClick={() => handleRemoveItem(index)}
|
|
385
|
+
disabled={readOnly}
|
|
386
|
+
type="button"
|
|
387
|
+
>
|
|
388
|
+
<X className="h-4 w-4" />
|
|
389
|
+
</Button>
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
|
|
393
|
+
{/* Add new item */}
|
|
394
|
+
<div className="flex items-center gap-2 p-2 border border-dashed rounded-md">
|
|
395
|
+
<div className="flex-1">{renderNewItemInput()}</div>
|
|
396
|
+
<Button
|
|
397
|
+
variant="outline"
|
|
398
|
+
size="sm"
|
|
399
|
+
onClick={handleAddItem}
|
|
400
|
+
disabled={readOnly || !newItemValue.trim()}
|
|
401
|
+
type="button"
|
|
402
|
+
>
|
|
403
|
+
<Plus className="h-4 w-4" />
|
|
404
|
+
</Button>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
{arrayValue.length === 0 && (
|
|
408
|
+
<div className="text-sm text-muted-foreground">
|
|
409
|
+
No items added yet. Use the input above to add items.
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function parseArrayValue(value: string): string[] {
|
|
417
|
+
if (!value) return [];
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(value);
|
|
421
|
+
return Array.isArray(parsed) ? parsed.map(String) : [String(parsed)];
|
|
422
|
+
} catch {
|
|
423
|
+
// If not JSON, try comma-separated values
|
|
424
|
+
return value.includes(',')
|
|
425
|
+
? value
|
|
426
|
+
.split(',')
|
|
427
|
+
.map(s => s.trim())
|
|
428
|
+
.filter(Boolean)
|
|
429
|
+
: value
|
|
430
|
+
? [value]
|
|
431
|
+
: [];
|
|
432
|
+
}
|
|
433
|
+
}
|