@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.
Files changed (35) hide show
  1. package/package.json +4 -4
  2. package/src/app/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx +1 -1
  3. package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +11 -78
  4. package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +11 -81
  5. package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +10 -77
  6. package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +12 -87
  7. package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +12 -87
  8. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +10 -80
  9. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +10 -79
  10. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +8 -6
  11. package/src/lib/components/data-input/combination-mode-input.tsx +52 -0
  12. package/src/lib/components/data-input/configurable-operation-list-input.tsx +433 -0
  13. package/src/lib/components/data-input/custom-field-list-input.tsx +297 -0
  14. package/src/lib/components/data-input/datetime-input.tsx +5 -2
  15. package/src/lib/components/data-input/default-relation-input.tsx +599 -0
  16. package/src/lib/components/data-input/index.ts +6 -0
  17. package/src/lib/components/data-input/product-multi-selector.tsx +426 -0
  18. package/src/lib/components/data-input/relation-selector.tsx +7 -6
  19. package/src/lib/components/data-input/select-with-options.tsx +84 -0
  20. package/src/lib/components/data-input/struct-form-input.tsx +324 -0
  21. package/src/lib/components/shared/configurable-operation-arg-input.tsx +365 -21
  22. package/src/lib/components/shared/configurable-operation-input.tsx +81 -41
  23. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +260 -0
  24. package/src/lib/components/shared/configurable-operation-selector.tsx +156 -0
  25. package/src/lib/components/shared/custom-fields-form.tsx +207 -36
  26. package/src/lib/components/shared/multi-select.tsx +1 -1
  27. package/src/lib/components/ui/form.tsx +4 -4
  28. package/src/lib/framework/extension-api/input-component-extensions.tsx +5 -1
  29. package/src/lib/framework/form-engine/form-schema-tools.spec.ts +472 -0
  30. package/src/lib/framework/form-engine/form-schema-tools.ts +340 -5
  31. package/src/lib/framework/form-engine/use-generated-form.tsx +24 -8
  32. package/src/lib/framework/form-engine/utils.ts +3 -9
  33. package/src/lib/framework/layout-engine/page-layout.tsx +11 -3
  34. package/src/lib/framework/page/use-detail-page.ts +3 -3
  35. package/src/lib/lib/utils.ts +26 -24
@@ -0,0 +1,297 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import { useLingui } from '@/vdb/lib/trans.js';
3
+ import {
4
+ closestCenter,
5
+ DndContext,
6
+ KeyboardSensor,
7
+ PointerSensor,
8
+ useSensor,
9
+ useSensors,
10
+ } from '@dnd-kit/core';
11
+ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
12
+ import {
13
+ arrayMove,
14
+ SortableContext,
15
+ sortableKeyboardCoordinates,
16
+ useSortable,
17
+ verticalListSortingStrategy,
18
+ } from '@dnd-kit/sortable';
19
+ import { CSS } from '@dnd-kit/utilities';
20
+ import { GripVertical, Plus, X } from 'lucide-react';
21
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
+ import { ControllerRenderProps } from 'react-hook-form';
23
+
24
+ interface ListItemWithId {
25
+ _id: string;
26
+ value: any;
27
+ }
28
+
29
+ interface CustomFieldListInputProps {
30
+ field: ControllerRenderProps<any, any>;
31
+ disabled?: boolean;
32
+ renderInput: (index: number, inputField: ControllerRenderProps<any, any>) => React.ReactNode;
33
+ defaultValue?: any;
34
+ }
35
+
36
+ interface SortableItemProps {
37
+ itemWithId: ListItemWithId;
38
+ index: number;
39
+ disabled?: boolean;
40
+ renderInput: (index: number, inputField: ControllerRenderProps<any, any>) => React.ReactNode;
41
+ onRemove: (id: string) => void;
42
+ onItemChange: (id: string, value: any) => void;
43
+ field: ControllerRenderProps<any, any>;
44
+ isFullWidth?: boolean;
45
+ }
46
+
47
+ function SortableItem({
48
+ itemWithId,
49
+ index,
50
+ disabled,
51
+ renderInput,
52
+ onRemove,
53
+ onItemChange,
54
+ field,
55
+ isFullWidth = false,
56
+ }: Readonly<SortableItemProps>) {
57
+ const { i18n } = useLingui();
58
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
59
+ id: itemWithId._id,
60
+ disabled,
61
+ });
62
+
63
+ const style = {
64
+ transform: CSS.Transform.toString(transform),
65
+ transition,
66
+ };
67
+
68
+ const DragHandle = !disabled && (
69
+ <div
70
+ {...attributes}
71
+ {...listeners}
72
+ className="cursor-move text-muted-foreground hover:text-foreground transition-colors"
73
+ title={i18n.t('Drag to reorder')}
74
+ >
75
+ <GripVertical className="h-4 w-4" />
76
+ </div>
77
+ );
78
+
79
+ const RemoveButton = !disabled && (
80
+ <Button
81
+ type="button"
82
+ variant="ghost"
83
+ size="sm"
84
+ onClick={() => onRemove(itemWithId._id)}
85
+ className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive transition-colors opacity-0 group-hover:opacity-100"
86
+ title={i18n.t('Remove item')}
87
+ >
88
+ <X className="h-3 w-3" />
89
+ </Button>
90
+ );
91
+
92
+ if (!isFullWidth) {
93
+ // Inline layout for single-line inputs
94
+ return (
95
+ <div
96
+ ref={setNodeRef}
97
+ style={style}
98
+ className={`group relative flex items-center gap-2 p-2 border rounded-lg bg-card hover:bg-accent/50 transition-colors ${
99
+ isDragging ? 'opacity-50 shadow-lg' : ''
100
+ }`}
101
+ >
102
+ {DragHandle}
103
+ <div className="flex-1">
104
+ {renderInput(index, {
105
+ name: `${field.name}.${index}`,
106
+ value: itemWithId.value,
107
+ onChange: value => onItemChange(itemWithId._id, value),
108
+ onBlur: field.onBlur,
109
+ ref: field.ref,
110
+ } as ControllerRenderProps<any, any>)}
111
+ </div>
112
+ {RemoveButton}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ // Full-width layout for complex inputs
118
+ return (
119
+ <div
120
+ ref={setNodeRef}
121
+ style={style}
122
+ className={`group relative border rounded-lg bg-card hover:bg-accent/50 transition-colors ${
123
+ isDragging ? 'opacity-50 shadow-lg' : ''
124
+ }`}
125
+ >
126
+ <div className="flex items-center justify-between px-3 py-2 border-b">
127
+ {DragHandle}
128
+ <div className="flex-1" />
129
+ {RemoveButton}
130
+ </div>
131
+ <div className="p-3">
132
+ {renderInput(index, {
133
+ name: `${field.name}.${index}`,
134
+ value: itemWithId.value,
135
+ onChange: value => onItemChange(itemWithId._id, value),
136
+ onBlur: field.onBlur,
137
+ ref: field.ref,
138
+ } as ControllerRenderProps<any, any>)}
139
+ </div>
140
+ </div>
141
+ );
142
+ }
143
+
144
+ // Generate unique IDs for list items
145
+ function generateId(): string {
146
+ return `item-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
147
+ }
148
+
149
+ // Convert flat array to array with stable IDs
150
+ function convertToItemsWithIds(values: any[], existingItems?: ListItemWithId[]): ListItemWithId[] {
151
+ if (!values || values.length === 0) return [];
152
+
153
+ return values.map((value, index) => {
154
+ // Try to reuse existing ID if the value matches and index is within bounds
155
+ const existingItem = existingItems?.[index];
156
+ if (existingItem && JSON.stringify(existingItem.value) === JSON.stringify(value)) {
157
+ return existingItem;
158
+ }
159
+
160
+ // Otherwise create new item with new ID
161
+ return {
162
+ _id: generateId(),
163
+ value,
164
+ };
165
+ });
166
+ }
167
+
168
+ // Convert array with IDs back to flat array
169
+ function convertToFlatArray(itemsWithIds: ListItemWithId[]): any[] {
170
+ return itemsWithIds.map(item => item.value);
171
+ }
172
+
173
+ export function CustomFieldListInput({
174
+ field,
175
+ disabled,
176
+ renderInput,
177
+ defaultValue,
178
+ isFullWidth = false,
179
+ }: CustomFieldListInputProps & { isFullWidth?: boolean }) {
180
+ const { i18n } = useLingui();
181
+ const sensors = useSensors(
182
+ useSensor(PointerSensor),
183
+ useSensor(KeyboardSensor, {
184
+ coordinateGetter: sortableKeyboardCoordinates,
185
+ }),
186
+ );
187
+
188
+ // Keep track of items with stable IDs
189
+ const [itemsWithIds, setItemsWithIds] = useState<ListItemWithId[]>(() =>
190
+ convertToItemsWithIds(field.value || []),
191
+ );
192
+
193
+ // Update items when field value changes externally (e.g., form reset, initial load)
194
+ useEffect(() => {
195
+ const newItems = convertToItemsWithIds(field.value || [], itemsWithIds);
196
+ if (
197
+ JSON.stringify(convertToFlatArray(newItems)) !== JSON.stringify(convertToFlatArray(itemsWithIds))
198
+ ) {
199
+ setItemsWithIds(newItems);
200
+ }
201
+ }, [field.value, itemsWithIds]);
202
+
203
+ const itemIds = useMemo(() => itemsWithIds.map(item => item._id), [itemsWithIds]);
204
+
205
+ const handleAddItem = useCallback(() => {
206
+ const newItem: ListItemWithId = {
207
+ _id: generateId(),
208
+ value: defaultValue ?? '',
209
+ };
210
+ const newItemsWithIds = [...itemsWithIds, newItem];
211
+ setItemsWithIds(newItemsWithIds);
212
+ field.onChange(convertToFlatArray(newItemsWithIds));
213
+ }, [itemsWithIds, defaultValue, field]);
214
+
215
+ const handleRemoveItem = useCallback(
216
+ (id: string) => {
217
+ const newItemsWithIds = itemsWithIds.filter(item => item._id !== id);
218
+ setItemsWithIds(newItemsWithIds);
219
+ field.onChange(convertToFlatArray(newItemsWithIds));
220
+ },
221
+ [itemsWithIds, field],
222
+ );
223
+
224
+ const handleItemChange = useCallback(
225
+ (id: string, value: any) => {
226
+ const newItemsWithIds = itemsWithIds.map(item => (item._id === id ? { ...item, value } : item));
227
+ setItemsWithIds(newItemsWithIds);
228
+ field.onChange(convertToFlatArray(newItemsWithIds));
229
+ },
230
+ [itemsWithIds, field],
231
+ );
232
+
233
+ const handleDragEnd = useCallback(
234
+ (event: any) => {
235
+ const { active, over } = event;
236
+
237
+ if (over && active.id !== over.id) {
238
+ const oldIndex = itemIds.indexOf(active.id);
239
+ const newIndex = itemIds.indexOf(over.id);
240
+
241
+ const newItemsWithIds = arrayMove(itemsWithIds, oldIndex, newIndex);
242
+ setItemsWithIds(newItemsWithIds);
243
+ field.onChange(convertToFlatArray(newItemsWithIds));
244
+ }
245
+ },
246
+ [itemIds, itemsWithIds, field],
247
+ );
248
+
249
+ const containerClasses = useMemo(() => {
250
+ const contentClasses =
251
+ 'overflow-y-auto resize-y border-b rounded bg-muted/30 bg-background p-1 space-y-1';
252
+
253
+ if (itemsWithIds.length === 0) {
254
+ return `hidden`;
255
+ } else if (itemsWithIds.length > 5) {
256
+ return `h-[200px] ${contentClasses}`;
257
+ } else {
258
+ return `min-h-[100px] ${contentClasses}`;
259
+ }
260
+ }, [itemsWithIds.length]);
261
+
262
+ return (
263
+ <div className="space-y-2">
264
+ <DndContext
265
+ sensors={sensors}
266
+ collisionDetection={closestCenter}
267
+ onDragEnd={handleDragEnd}
268
+ modifiers={[restrictToVerticalAxis]}
269
+ >
270
+ <div className={containerClasses}>
271
+ <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
272
+ {itemsWithIds.map((itemWithId, index) => (
273
+ <SortableItem
274
+ key={itemWithId._id}
275
+ itemWithId={itemWithId}
276
+ index={index}
277
+ disabled={disabled}
278
+ renderInput={renderInput}
279
+ onRemove={handleRemoveItem}
280
+ onItemChange={handleItemChange}
281
+ field={field}
282
+ isFullWidth={isFullWidth}
283
+ />
284
+ ))}
285
+ </SortableContext>
286
+ </div>
287
+ </DndContext>
288
+
289
+ {!disabled && (
290
+ <Button type="button" variant="outline" size="sm" onClick={handleAddItem} className="w-full">
291
+ <Plus className="h-4 w-4 mr-2" />
292
+ {i18n.t('Add item')}
293
+ </Button>
294
+ )}
295
+ </div>
296
+ );
297
+ }
@@ -13,9 +13,11 @@ import { CalendarClock } from 'lucide-react';
13
13
  export interface DateTimeInputProps {
14
14
  value: Date | string | undefined;
15
15
  onChange: (value: Date) => void;
16
+ disabled?: boolean;
16
17
  }
17
18
 
18
19
  export function DateTimeInput(props: DateTimeInputProps) {
20
+ const { disabled = false } = props;
19
21
  const date = props.value && props.value instanceof Date ? props.value.toISOString() : (props.value ?? '');
20
22
  const [isOpen, setIsOpen] = React.useState(false);
21
23
 
@@ -42,12 +44,13 @@ export function DateTimeInput(props: DateTimeInputProps) {
42
44
  };
43
45
 
44
46
  return (
45
- <Popover open={isOpen} onOpenChange={setIsOpen}>
47
+ <Popover open={isOpen} onOpenChange={disabled ? undefined : setIsOpen}>
46
48
  <PopoverTrigger asChild>
47
49
  <Button
48
50
  variant="outline"
51
+ disabled={disabled}
49
52
  className={cn(
50
- 'w-full justify-start text-left font-normal',
53
+ 'w-full justify-start text-left font-normal shadow-xs',
51
54
  !date && 'text-muted-foreground',
52
55
  )}
53
56
  >