@vendure/dashboard 3.5.1-master-202511110232 → 3.5.1-master-202511130232

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.
@@ -1,13 +1,144 @@
1
- import { X } from 'lucide-react';
2
- import { KeyboardEvent, useId, useRef, useState } from 'react';
1
+ import { GripVertical, X } from 'lucide-react';
2
+ import { KeyboardEvent, useEffect, useId, useRef, useState } from 'react';
3
3
 
4
4
  import { Badge } from '@/vdb/components/ui/badge.js';
5
5
  import { Input } from '@/vdb/components/ui/input.js';
6
6
  import type { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
7
7
  import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
8
8
  import { cn } from '@/vdb/lib/utils.js';
9
+ import {
10
+ closestCenter,
11
+ DndContext,
12
+ type DragEndEvent,
13
+ KeyboardSensor,
14
+ PointerSensor,
15
+ useSensor,
16
+ useSensors,
17
+ } from '@dnd-kit/core';
18
+ import {
19
+ arrayMove,
20
+ SortableContext,
21
+ sortableKeyboardCoordinates,
22
+ useSortable,
23
+ verticalListSortingStrategy,
24
+ } from '@dnd-kit/sortable';
25
+ import { CSS } from '@dnd-kit/utilities';
9
26
  import { useLingui } from '@lingui/react';
10
27
 
28
+ interface SortableItemProps {
29
+ id: string;
30
+ item: string;
31
+ isDisabled: boolean;
32
+ isEditing: boolean;
33
+ onRemove: () => void;
34
+ onEdit: () => void;
35
+ onSave: (newValue: string) => void;
36
+ }
37
+
38
+ function SortableItem({ id, item, isDisabled, isEditing, onRemove, onEdit, onSave }: SortableItemProps) {
39
+ const [editValue, setEditValue] = useState(item);
40
+ const inputRef = useRef<HTMLInputElement>(null);
41
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
42
+ id,
43
+ });
44
+
45
+ const style = {
46
+ transform: CSS.Transform.toString(transform),
47
+ transition,
48
+ };
49
+
50
+ const handleSave = () => {
51
+ const trimmedValue = editValue.trim();
52
+ if (trimmedValue && trimmedValue !== item) {
53
+ onSave(trimmedValue);
54
+ } else {
55
+ setEditValue(item);
56
+ onSave(item);
57
+ }
58
+ };
59
+
60
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
61
+ if (e.key === 'Enter') {
62
+ e.preventDefault();
63
+ handleSave();
64
+ } else if (e.key === 'Escape') {
65
+ setEditValue(item);
66
+ onSave(item);
67
+ }
68
+ };
69
+
70
+ // Focus and select input when entering edit mode
71
+ useEffect(() => {
72
+ if (isEditing && inputRef.current) {
73
+ inputRef.current.focus();
74
+ inputRef.current.select();
75
+ }
76
+ }, [isEditing]);
77
+
78
+ return (
79
+ <Badge
80
+ ref={setNodeRef}
81
+ style={style}
82
+ variant="secondary"
83
+ className={cn(
84
+ isDragging && 'opacity-50',
85
+ 'flex items-center gap-1',
86
+ isEditing && 'border-muted-foreground/30',
87
+ )}
88
+ >
89
+ {!isDisabled && (
90
+ <button
91
+ type="button"
92
+ className={cn(
93
+ 'cursor-grab active:cursor-grabbing text-muted-foreground',
94
+ 'hover:bg-muted rounded p-0.5',
95
+ )}
96
+ {...attributes}
97
+ {...listeners}
98
+ aria-label={`Drag ${item}`}
99
+ >
100
+ <GripVertical className="h-3 w-3" />
101
+ </button>
102
+ )}
103
+ {isEditing ? (
104
+ <input
105
+ ref={inputRef}
106
+ type="text"
107
+ value={editValue}
108
+ onChange={e => setEditValue(e.target.value)}
109
+ onKeyDown={handleKeyDown}
110
+ onBlur={handleSave}
111
+ className="bg-transparent border-none outline-none focus:ring-0 p-0 h-auto min-w-[60px] w-auto"
112
+ style={{ width: `${Math.max(editValue.length * 8, 60)}px` }}
113
+ />
114
+ ) : (
115
+ <span
116
+ onClick={!isDisabled ? onEdit : undefined}
117
+ className={cn(!isDisabled && 'cursor-text hover:underline')}
118
+ >
119
+ {item}
120
+ </span>
121
+ )}
122
+ {!isDisabled && (
123
+ <button
124
+ type="button"
125
+ onClick={e => {
126
+ e.stopPropagation();
127
+ onRemove();
128
+ }}
129
+ className={cn(
130
+ 'ml-1 rounded-full outline-none ring-offset-background text-muted-foreground',
131
+ 'hover:bg-muted focus:ring-2 focus:ring-ring focus:ring-offset-2',
132
+ )}
133
+ aria-label={`Remove ${item}`}
134
+ >
135
+ <X className="h-3 w-3" />
136
+ </button>
137
+ )}
138
+ </Badge>
139
+ );
140
+ }
141
+
11
142
  export function StringListInput({
12
143
  value,
13
144
  onChange,
@@ -17,13 +148,21 @@ export function StringListInput({
17
148
  fieldDef,
18
149
  }: DashboardFormComponentProps) {
19
150
  const [inputValue, setInputValue] = useState('');
151
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
20
152
  const inputRef = useRef<HTMLInputElement>(null);
21
153
  const { i18n } = useLingui();
22
- const isDisabled = isReadonlyField(fieldDef) || disabled;
154
+ const isDisabled = isReadonlyField(fieldDef) || disabled || false;
23
155
  const id = useId();
24
156
 
25
157
  const items = Array.isArray(value) ? value : [];
26
158
 
159
+ const sensors = useSensors(
160
+ useSensor(PointerSensor),
161
+ useSensor(KeyboardSensor, {
162
+ coordinateGetter: sortableKeyboardCoordinates,
163
+ }),
164
+ );
165
+
27
166
  const addItem = (item: string) => {
28
167
  const trimmedItem = item.trim();
29
168
  if (trimmedItem) {
@@ -36,6 +175,24 @@ export function StringListInput({
36
175
  onChange(items.filter((_, index) => index !== indexToRemove));
37
176
  };
38
177
 
178
+ const editItem = (index: number, newValue: string) => {
179
+ const newItems = [...items];
180
+ newItems[index] = newValue;
181
+ onChange(newItems);
182
+ setEditingIndex(null);
183
+ };
184
+
185
+ const handleDragEnd = (event: DragEndEvent) => {
186
+ const { active, over } = event;
187
+
188
+ if (over && active.id !== over.id) {
189
+ const oldIndex = items.findIndex((_, idx) => `${id}-${idx}` === active.id);
190
+ const newIndex = items.findIndex((_, idx) => `${id}-${idx}` === over.id);
191
+
192
+ onChange(arrayMove(items, oldIndex, newIndex));
193
+ }
194
+ };
195
+
39
196
  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
40
197
  if (e.key === 'Enter' || e.key === ',') {
41
198
  e.preventDefault();
@@ -74,29 +231,27 @@ export function StringListInput({
74
231
  className="min-w-[120px]"
75
232
  />
76
233
  )}
77
- <div className="flex flex-wrap gap-1 items-start justify-start">
78
- {items.map((item, index) => (
79
- <Badge key={id + index} variant="secondary">
80
- <span>{item}</span>
81
- {!isDisabled && (
82
- <button
83
- type="button"
84
- onClick={e => {
85
- e.stopPropagation();
86
- removeItem(index);
87
- }}
88
- className={cn(
89
- 'ml-1 rounded-full outline-none ring-offset-background',
90
- 'hover:bg-muted focus:ring-2 focus:ring-ring focus:ring-offset-2',
91
- )}
92
- aria-label={`Remove ${item}`}
93
- >
94
- <X className="h-3 w-3" />
95
- </button>
96
- )}
97
- </Badge>
98
- ))}
99
- </div>
234
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
235
+ <SortableContext
236
+ items={items.map((_, index) => `${id}-${index}`)}
237
+ strategy={verticalListSortingStrategy}
238
+ >
239
+ <div className="flex flex-wrap gap-1 items-start justify-start">
240
+ {items.map((item, index) => (
241
+ <SortableItem
242
+ key={`${id}-${index}`}
243
+ id={`${id}-${index}`}
244
+ item={item}
245
+ isDisabled={isDisabled}
246
+ isEditing={editingIndex === index}
247
+ onRemove={() => removeItem(index)}
248
+ onEdit={() => setEditingIndex(index)}
249
+ onSave={newValue => editItem(index, newValue)}
250
+ />
251
+ ))}
252
+ </div>
253
+ </SortableContext>
254
+ </DndContext>
100
255
  </div>
101
256
  );
102
257
  }
@@ -72,7 +72,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Readon
72
72
  const shouldShowTabs = useMemo(() => {
73
73
  if (!customFields) return false;
74
74
  const hasTabbedFields = customFields.some(field => field.ui?.tab);
75
- return hasTabbedFields || groupedFields.length > 1;
75
+ return hasTabbedFields && groupedFields.length > 1;
76
76
  }, [customFields, groupedFields.length]);
77
77
 
78
78
  if (!shouldShowTabs) {