@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
|
@@ -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
|
>
|