@vendure/dashboard 3.4.1-master-202508210231 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +152 -152
  2. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +2 -1
  3. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +1 -0
  4. package/src/lib/components/data-input/affixed-input.tsx +19 -5
  5. package/src/lib/components/data-input/boolean-input.tsx +9 -0
  6. package/src/lib/components/data-input/checkbox-input.tsx +8 -0
  7. package/src/lib/components/data-input/combination-mode-input.tsx +11 -2
  8. package/src/lib/components/data-input/configurable-operation-list-input.tsx +26 -401
  9. package/src/lib/components/data-input/custom-field-list-input.tsx +18 -25
  10. package/src/lib/components/data-input/customer-group-input.tsx +7 -11
  11. package/src/lib/components/data-input/datetime-input.tsx +9 -13
  12. package/src/lib/components/data-input/default-relation-input.tsx +29 -9
  13. package/src/lib/components/data-input/facet-value-input.tsx +15 -13
  14. package/src/lib/components/data-input/index.ts +2 -2
  15. package/src/lib/components/data-input/money-input.tsx +27 -20
  16. package/src/lib/components/data-input/number-input.tsx +48 -0
  17. package/src/lib/components/data-input/password-input.tsx +16 -0
  18. package/src/lib/components/data-input/{product-multi-selector.tsx → product-multi-selector-input.tsx} +8 -15
  19. package/src/lib/components/data-input/relation-input.tsx +7 -6
  20. package/src/lib/components/data-input/rich-text-input.tsx +10 -13
  21. package/src/lib/components/data-input/select-with-options.tsx +29 -17
  22. package/src/lib/components/data-input/struct-form-input.tsx +54 -59
  23. package/src/lib/components/data-input/text-input.tsx +9 -0
  24. package/src/lib/components/data-input/textarea-input.tsx +16 -0
  25. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +3 -0
  26. package/src/lib/components/data-table/use-generated-columns.tsx +16 -5
  27. package/src/lib/components/shared/configurable-operation-arg-input.tsx +3 -10
  28. package/src/lib/components/shared/configurable-operation-input.tsx +1 -6
  29. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +8 -5
  30. package/src/lib/components/shared/configurable-operation-selector.tsx +5 -5
  31. package/src/lib/components/shared/custom-fields-form.tsx +20 -49
  32. package/src/lib/components/shared/multi-select.tsx +1 -1
  33. package/src/lib/framework/component-registry/component-registry.tsx +9 -32
  34. package/src/lib/framework/component-registry/display-component.tsx +28 -0
  35. package/src/lib/framework/extension-api/display-component-extensions.tsx +0 -14
  36. package/src/lib/framework/extension-api/input-component-extensions.tsx +52 -34
  37. package/src/lib/framework/extension-api/logic/data-table.ts +4 -27
  38. package/src/lib/framework/extension-api/logic/form-components.ts +3 -2
  39. package/src/lib/framework/extension-api/types/detail-forms.ts +2 -38
  40. package/src/lib/framework/extension-api/types/form-components.ts +2 -4
  41. package/src/lib/framework/form-engine/custom-form-component-extensions.ts +0 -23
  42. package/src/lib/framework/form-engine/custom-form-component.tsx +8 -25
  43. package/src/lib/framework/form-engine/default-input-for-type.tsx +35 -0
  44. package/src/lib/framework/form-engine/form-control-adapter.tsx +192 -0
  45. package/src/lib/framework/form-engine/form-engine-types.ts +163 -0
  46. package/src/lib/framework/form-engine/form-schema-tools.ts +55 -71
  47. package/src/lib/framework/form-engine/overridden-form-component.tsx +2 -2
  48. package/src/lib/framework/form-engine/utils.ts +223 -0
  49. package/src/lib/{components/shared → framework/form-engine}/value-transformers.ts +9 -9
  50. package/src/lib/framework/registry/registry-types.ts +3 -5
  51. package/src/lib/graphql/graphql-env.d.ts +11 -7
  52. package/src/lib/index.ts +28 -1
  53. package/src/lib/providers/server-config.tsx +1 -0
  54. package/src/lib/components/shared/direct-form-component-map.tsx +0 -393
  55. package/src/lib/components/shared/universal-field-definition.ts +0 -118
  56. package/src/lib/components/shared/universal-form-input.tsx +0 -175
  57. package/src/lib/components/shared/universal-input-components.tsx +0 -291
  58. package/src/lib/framework/component-registry/dynamic-component.tsx +0 -58
@@ -1,414 +1,39 @@
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
- }
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
- definition,
10
+ fieldDef,
49
11
  value,
50
12
  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
- );
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
- {/* 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
- )}
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 function CustomFieldListInput({
174
- field,
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(field.value || []),
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(field.value || [], itemsWithIds);
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
- }, [field.value, itemsWithIds]);
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
- field.onChange(convertToFlatArray(newItemsWithIds));
213
- }, [itemsWithIds, defaultValue, field]);
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
- field.onChange(convertToFlatArray(newItemsWithIds));
213
+ onChange(convertToFlatArray(newItemsWithIds));
220
214
  },
221
- [itemsWithIds, field],
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
- field.onChange(convertToFlatArray(newItemsWithIds));
222
+ onChange(convertToFlatArray(newItemsWithIds));
229
223
  },
230
- [itemsWithIds, field],
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
- field.onChange(convertToFlatArray(newItemsWithIds));
237
+ onChange(convertToFlatArray(newItemsWithIds));
244
238
  }
245
239
  },
246
- [itemIds, itemsWithIds, field],
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={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 interface CustomerGroupInputProps {
24
- value: string;
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
- props.onChange(JSON.stringify(Array.from(newIds)));
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
- props.onChange(JSON.stringify(Array.from(newIds)));
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={props.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 interface DateTimeInputProps {
14
- value: Date | string | undefined;
15
- onChange: (value: Date) => void;
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
- props.onChange(selectedDate);
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
- props.onChange(newDate);
38
+ onChange(newDate);
43
39
  }
44
40
  };
45
41
 
46
42
  return (
47
- <Popover open={isOpen} onOpenChange={disabled ? undefined : setIsOpen}>
43
+ <Popover open={isOpen} onOpenChange={readOnly ? undefined : setIsOpen}>
48
44
  <PopoverTrigger asChild>
49
45
  <Button
50
46
  variant="outline"
51
- disabled={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',