@vendure/dashboard 3.4.1-master-202508210231 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +152 -152
- package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +2 -1
- package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +1 -0
- package/src/lib/components/data-input/affixed-input.tsx +19 -5
- package/src/lib/components/data-input/boolean-input.tsx +9 -0
- package/src/lib/components/data-input/checkbox-input.tsx +8 -0
- package/src/lib/components/data-input/combination-mode-input.tsx +11 -2
- package/src/lib/components/data-input/configurable-operation-list-input.tsx +26 -401
- package/src/lib/components/data-input/custom-field-list-input.tsx +18 -25
- package/src/lib/components/data-input/customer-group-input.tsx +7 -11
- package/src/lib/components/data-input/datetime-input.tsx +9 -13
- package/src/lib/components/data-input/default-relation-input.tsx +29 -9
- package/src/lib/components/data-input/facet-value-input.tsx +15 -13
- package/src/lib/components/data-input/index.ts +2 -2
- package/src/lib/components/data-input/money-input.tsx +27 -20
- package/src/lib/components/data-input/number-input.tsx +48 -0
- package/src/lib/components/data-input/password-input.tsx +16 -0
- package/src/lib/components/data-input/{product-multi-selector.tsx → product-multi-selector-input.tsx} +8 -15
- package/src/lib/components/data-input/relation-input.tsx +7 -6
- package/src/lib/components/data-input/rich-text-input.tsx +10 -13
- package/src/lib/components/data-input/select-with-options.tsx +29 -17
- package/src/lib/components/data-input/struct-form-input.tsx +54 -59
- package/src/lib/components/data-input/text-input.tsx +9 -0
- package/src/lib/components/data-input/textarea-input.tsx +16 -0
- package/src/lib/components/data-table/filters/data-table-number-filter.tsx +3 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +16 -5
- package/src/lib/components/shared/configurable-operation-arg-input.tsx +3 -10
- package/src/lib/components/shared/configurable-operation-input.tsx +1 -6
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +8 -5
- package/src/lib/components/shared/configurable-operation-selector.tsx +5 -5
- package/src/lib/components/shared/custom-fields-form.tsx +20 -49
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/framework/component-registry/component-registry.tsx +9 -32
- package/src/lib/framework/component-registry/display-component.tsx +28 -0
- package/src/lib/framework/extension-api/display-component-extensions.tsx +0 -14
- package/src/lib/framework/extension-api/input-component-extensions.tsx +52 -34
- package/src/lib/framework/extension-api/logic/data-table.ts +4 -27
- package/src/lib/framework/extension-api/logic/form-components.ts +3 -2
- package/src/lib/framework/extension-api/types/detail-forms.ts +2 -38
- package/src/lib/framework/extension-api/types/form-components.ts +2 -4
- package/src/lib/framework/form-engine/custom-form-component-extensions.ts +0 -23
- package/src/lib/framework/form-engine/custom-form-component.tsx +8 -25
- package/src/lib/framework/form-engine/default-input-for-type.tsx +35 -0
- package/src/lib/framework/form-engine/form-control-adapter.tsx +192 -0
- package/src/lib/framework/form-engine/form-engine-types.ts +163 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +55 -71
- package/src/lib/framework/form-engine/overridden-form-component.tsx +2 -2
- package/src/lib/framework/form-engine/utils.ts +223 -0
- package/src/lib/{components/shared → framework/form-engine}/value-transformers.ts +9 -9
- package/src/lib/framework/registry/registry-types.ts +3 -5
- package/src/lib/graphql/graphql-env.d.ts +11 -7
- package/src/lib/index.ts +28 -1
- package/src/lib/providers/server-config.tsx +1 -0
- package/src/lib/components/shared/direct-form-component-map.tsx +0 -393
- package/src/lib/components/shared/universal-field-definition.ts +0 -118
- package/src/lib/components/shared/universal-form-input.tsx +0 -175
- package/src/lib/components/shared/universal-input-components.tsx +0 -291
- package/src/lib/framework/component-registry/dynamic-component.tsx +0 -58
|
@@ -1,414 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
}
|
|
1
|
+
import { FormControlAdapter } from '@/vdb/framework/form-engine/form-control-adapter.js';
|
|
2
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
3
|
+
import { isCustomFieldConfig } from '@/vdb/framework/form-engine/utils.js';
|
|
4
|
+
import { ControllerRenderProps } from 'react-hook-form';
|
|
18
5
|
|
|
19
6
|
/**
|
|
20
7
|
* 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
8
|
*/
|
|
47
9
|
export function ConfigurableOperationListInput({
|
|
48
|
-
|
|
10
|
+
fieldDef,
|
|
49
11
|
value,
|
|
50
12
|
onChange,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
);
|
|
13
|
+
}: Readonly<DashboardFormComponentProps>) {
|
|
14
|
+
if (!fieldDef || isCustomFieldConfig(fieldDef)) {
|
|
15
|
+
return null;
|
|
373
16
|
}
|
|
374
|
-
|
|
17
|
+
const arrayValue = parseArrayValue(value);
|
|
375
18
|
return (
|
|
376
19
|
<div className="space-y-2">
|
|
377
|
-
{
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
>
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
)}
|
|
20
|
+
{arrayValue.map((item, index) => {
|
|
21
|
+
const field = {
|
|
22
|
+
value: item,
|
|
23
|
+
onChange,
|
|
24
|
+
disabled: false,
|
|
25
|
+
onBlur: () => {},
|
|
26
|
+
name: fieldDef.name,
|
|
27
|
+
ref: () => {},
|
|
28
|
+
} satisfies ControllerRenderProps<any, any>;
|
|
29
|
+
return (
|
|
30
|
+
<div key={`array-item-${index}`} className="flex items-center gap-2">
|
|
31
|
+
<div className="flex-1">
|
|
32
|
+
<FormControlAdapter field={field} fieldDef={fieldDef} valueMode="native" />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
412
37
|
</div>
|
|
413
38
|
);
|
|
414
39
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
2
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
2
3
|
import { useLingui } from '@/vdb/lib/trans.js';
|
|
3
4
|
import {
|
|
4
5
|
closestCenter,
|
|
@@ -26,9 +27,7 @@ interface ListItemWithId {
|
|
|
26
27
|
value: any;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
interface CustomFieldListInputProps {
|
|
30
|
-
field: ControllerRenderProps<any, any>;
|
|
31
|
-
disabled?: boolean;
|
|
30
|
+
interface CustomFieldListInputProps extends DashboardFormComponentProps {
|
|
32
31
|
renderInput: (index: number, inputField: ControllerRenderProps<any, any>) => React.ReactNode;
|
|
33
32
|
defaultValue?: any;
|
|
34
33
|
}
|
|
@@ -148,7 +147,7 @@ function generateId(): string {
|
|
|
148
147
|
|
|
149
148
|
// Convert flat array to array with stable IDs
|
|
150
149
|
function convertToItemsWithIds(values: any[], existingItems?: ListItemWithId[]): ListItemWithId[] {
|
|
151
|
-
if (!values || values.length === 0) return [];
|
|
150
|
+
if (!values || !Array.isArray(values) || values.length === 0) return [];
|
|
152
151
|
|
|
153
152
|
return values.map((value, index) => {
|
|
154
153
|
// Try to reuse existing ID if the value matches and index is within bounds
|
|
@@ -170,13 +169,8 @@ function convertToFlatArray(itemsWithIds: ListItemWithId[]): any[] {
|
|
|
170
169
|
return itemsWithIds.map(item => item.value);
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
export
|
|
174
|
-
|
|
175
|
-
disabled,
|
|
176
|
-
renderInput,
|
|
177
|
-
defaultValue,
|
|
178
|
-
isFullWidth = false,
|
|
179
|
-
}: CustomFieldListInputProps & { isFullWidth?: boolean }) {
|
|
172
|
+
export const CustomFieldListInput = ({ renderInput, defaultValue, ...fieldProps }: CustomFieldListInputProps) => {
|
|
173
|
+
const { value, onChange, disabled } = fieldProps;
|
|
180
174
|
const { i18n } = useLingui();
|
|
181
175
|
const sensors = useSensors(
|
|
182
176
|
useSensor(PointerSensor),
|
|
@@ -187,18 +181,18 @@ export function CustomFieldListInput({
|
|
|
187
181
|
|
|
188
182
|
// Keep track of items with stable IDs
|
|
189
183
|
const [itemsWithIds, setItemsWithIds] = useState<ListItemWithId[]>(() =>
|
|
190
|
-
convertToItemsWithIds(
|
|
184
|
+
convertToItemsWithIds(value || []),
|
|
191
185
|
);
|
|
192
186
|
|
|
193
187
|
// Update items when field value changes externally (e.g., form reset, initial load)
|
|
194
188
|
useEffect(() => {
|
|
195
|
-
const newItems = convertToItemsWithIds(
|
|
189
|
+
const newItems = convertToItemsWithIds(value || [], itemsWithIds);
|
|
196
190
|
if (
|
|
197
191
|
JSON.stringify(convertToFlatArray(newItems)) !== JSON.stringify(convertToFlatArray(itemsWithIds))
|
|
198
192
|
) {
|
|
199
193
|
setItemsWithIds(newItems);
|
|
200
194
|
}
|
|
201
|
-
}, [
|
|
195
|
+
}, [value, itemsWithIds]);
|
|
202
196
|
|
|
203
197
|
const itemIds = useMemo(() => itemsWithIds.map(item => item._id), [itemsWithIds]);
|
|
204
198
|
|
|
@@ -209,25 +203,25 @@ export function CustomFieldListInput({
|
|
|
209
203
|
};
|
|
210
204
|
const newItemsWithIds = [...itemsWithIds, newItem];
|
|
211
205
|
setItemsWithIds(newItemsWithIds);
|
|
212
|
-
|
|
213
|
-
}, [itemsWithIds, defaultValue,
|
|
206
|
+
onChange(convertToFlatArray(newItemsWithIds));
|
|
207
|
+
}, [itemsWithIds, defaultValue, onChange]);
|
|
214
208
|
|
|
215
209
|
const handleRemoveItem = useCallback(
|
|
216
210
|
(id: string) => {
|
|
217
211
|
const newItemsWithIds = itemsWithIds.filter(item => item._id !== id);
|
|
218
212
|
setItemsWithIds(newItemsWithIds);
|
|
219
|
-
|
|
213
|
+
onChange(convertToFlatArray(newItemsWithIds));
|
|
220
214
|
},
|
|
221
|
-
[itemsWithIds,
|
|
215
|
+
[itemsWithIds, onChange],
|
|
222
216
|
);
|
|
223
217
|
|
|
224
218
|
const handleItemChange = useCallback(
|
|
225
219
|
(id: string, value: any) => {
|
|
226
220
|
const newItemsWithIds = itemsWithIds.map(item => (item._id === id ? { ...item, value } : item));
|
|
227
221
|
setItemsWithIds(newItemsWithIds);
|
|
228
|
-
|
|
222
|
+
onChange(convertToFlatArray(newItemsWithIds));
|
|
229
223
|
},
|
|
230
|
-
[itemsWithIds,
|
|
224
|
+
[itemsWithIds, onChange],
|
|
231
225
|
);
|
|
232
226
|
|
|
233
227
|
const handleDragEnd = useCallback(
|
|
@@ -240,10 +234,10 @@ export function CustomFieldListInput({
|
|
|
240
234
|
|
|
241
235
|
const newItemsWithIds = arrayMove(itemsWithIds, oldIndex, newIndex);
|
|
242
236
|
setItemsWithIds(newItemsWithIds);
|
|
243
|
-
|
|
237
|
+
onChange(convertToFlatArray(newItemsWithIds));
|
|
244
238
|
}
|
|
245
239
|
},
|
|
246
|
-
[itemIds, itemsWithIds,
|
|
240
|
+
[itemIds, itemsWithIds, onChange],
|
|
247
241
|
);
|
|
248
242
|
|
|
249
243
|
const containerClasses = useMemo(() => {
|
|
@@ -278,8 +272,7 @@ export function CustomFieldListInput({
|
|
|
278
272
|
renderInput={renderInput}
|
|
279
273
|
onRemove={handleRemoveItem}
|
|
280
274
|
onItemChange={handleItemChange}
|
|
281
|
-
field={
|
|
282
|
-
isFullWidth={isFullWidth}
|
|
275
|
+
field={fieldProps}
|
|
283
276
|
/>
|
|
284
277
|
))}
|
|
285
278
|
</SortableContext>
|
|
@@ -294,4 +287,4 @@ export function CustomFieldListInput({
|
|
|
294
287
|
)}
|
|
295
288
|
</div>
|
|
296
289
|
);
|
|
297
|
-
}
|
|
290
|
+
};
|
|
@@ -4,6 +4,8 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
4
4
|
import { CustomerGroupChip } from '../shared/customer-group-chip.js';
|
|
5
5
|
import { CustomerGroupSelector } from '../shared/customer-group-selector.js';
|
|
6
6
|
|
|
7
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
8
|
+
|
|
7
9
|
const customerGroupsDocument = graphql(`
|
|
8
10
|
query GetCustomerGroups($options: CustomerGroupListOptions) {
|
|
9
11
|
customerGroups(options: $options) {
|
|
@@ -20,14 +22,8 @@ export interface CustomerGroup {
|
|
|
20
22
|
name: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export
|
|
24
|
-
value
|
|
25
|
-
onChange: (value: string) => void;
|
|
26
|
-
readOnly?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function CustomerGroupInput(props: CustomerGroupInputProps) {
|
|
30
|
-
const ids = decodeIds(props.value);
|
|
25
|
+
export function CustomerGroupInput({ value, onChange, disabled }: Readonly<DashboardFormComponentProps>) {
|
|
26
|
+
const ids = decodeIds(value);
|
|
31
27
|
const { data: groups } = useQuery({
|
|
32
28
|
queryKey: ['customerGroups', ids],
|
|
33
29
|
queryFn: () =>
|
|
@@ -42,12 +38,12 @@ export function CustomerGroupInput(props: CustomerGroupInputProps) {
|
|
|
42
38
|
|
|
43
39
|
const onValueSelectHandler = (value: CustomerGroup) => {
|
|
44
40
|
const newIds = new Set([...ids, value.id]);
|
|
45
|
-
|
|
41
|
+
onChange(JSON.stringify(Array.from(newIds)));
|
|
46
42
|
};
|
|
47
43
|
|
|
48
44
|
const onValueRemoveHandler = (id: string) => {
|
|
49
45
|
const newIds = new Set(ids.filter(existingId => existingId !== id));
|
|
50
|
-
|
|
46
|
+
onChange(JSON.stringify(Array.from(newIds)));
|
|
51
47
|
};
|
|
52
48
|
|
|
53
49
|
return (
|
|
@@ -58,7 +54,7 @@ export function CustomerGroupInput(props: CustomerGroupInputProps) {
|
|
|
58
54
|
))}
|
|
59
55
|
</div>
|
|
60
56
|
|
|
61
|
-
<CustomerGroupSelector onSelect={onValueSelectHandler} readOnly={
|
|
57
|
+
<CustomerGroupSelector onSelect={onValueSelectHandler} readOnly={disabled} />
|
|
62
58
|
</div>
|
|
63
59
|
);
|
|
64
60
|
}
|
|
@@ -7,24 +7,20 @@ import { Button } from '@/vdb/components/ui/button.js';
|
|
|
7
7
|
import { Calendar } from '@/vdb/components/ui/calendar.js';
|
|
8
8
|
import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
|
|
9
9
|
import { ScrollArea, ScrollBar } from '@/vdb/components/ui/scroll-area.js';
|
|
10
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
10
11
|
import { cn } from '@/vdb/lib/utils.js';
|
|
11
12
|
import { CalendarClock } from 'lucide-react';
|
|
13
|
+
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
12
14
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
disabled?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function DateTimeInput(props: DateTimeInputProps) {
|
|
20
|
-
const { disabled = false } = props;
|
|
21
|
-
const date = props.value && props.value instanceof Date ? props.value.toISOString() : (props.value ?? '');
|
|
15
|
+
export function DateTimeInput({ value, onChange, fieldDef }: Readonly<DashboardFormComponentProps>) {
|
|
16
|
+
const readOnly = isReadonlyField(fieldDef);
|
|
17
|
+
const date = value && value instanceof Date ? value.toISOString() : (value ?? '');
|
|
22
18
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
23
19
|
|
|
24
20
|
const hours = Array.from({ length: 12 }, (_, i) => i + 1);
|
|
25
21
|
const handleDateSelect = (selectedDate: Date | undefined) => {
|
|
26
22
|
if (selectedDate) {
|
|
27
|
-
|
|
23
|
+
onChange(selectedDate.toISOString());
|
|
28
24
|
}
|
|
29
25
|
};
|
|
30
26
|
|
|
@@ -39,16 +35,16 @@ export function DateTimeInput(props: DateTimeInputProps) {
|
|
|
39
35
|
const currentHours = newDate.getHours();
|
|
40
36
|
newDate.setHours(value === 'PM' ? currentHours + 12 : currentHours - 12);
|
|
41
37
|
}
|
|
42
|
-
|
|
38
|
+
onChange(newDate);
|
|
43
39
|
}
|
|
44
40
|
};
|
|
45
41
|
|
|
46
42
|
return (
|
|
47
|
-
<Popover open={isOpen} onOpenChange={
|
|
43
|
+
<Popover open={isOpen} onOpenChange={readOnly ? undefined : setIsOpen}>
|
|
48
44
|
<PopoverTrigger asChild>
|
|
49
45
|
<Button
|
|
50
46
|
variant="outline"
|
|
51
|
-
disabled={
|
|
47
|
+
disabled={readOnly}
|
|
52
48
|
className={cn(
|
|
53
49
|
'w-full justify-start text-left font-normal shadow-xs',
|
|
54
50
|
!date && 'text-muted-foreground',
|