@vendure/dashboard 3.5.1-master-202511120232 → 3.5.1-master-202511140232

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
  }
@@ -163,7 +163,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
163
163
  if (!id) {
164
164
  throw new Error('Column id is required');
165
165
  }
166
- finalColumns.push(columnHelper.accessor(id as any, { ...column, id, enableColumnFilter: false }));
166
+ finalColumns.push(columnHelper.accessor(id as any, { enableColumnFilter: false, ...column, id }));
167
167
  }
168
168
 
169
169
  if (defaultColumnOrder) {
@@ -85,7 +85,8 @@ const ALL_LANGUAGE_CODES = [
85
85
  'hi',
86
86
  'sv',
87
87
  'da',
88
- 'no',
88
+ 'nb',
89
+ 'nn',
89
90
  'fi',
90
91
  ];
91
92
 
@@ -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) {
@@ -127,7 +127,7 @@ export function MultiSelect<T extends boolean>(props: MultiSelectProps<T>) {
127
127
  return (
128
128
  <Popover>
129
129
  <PopoverTrigger asChild>{renderTrigger()}</PopoverTrigger>
130
- <PopoverContent className="w-[200px] p-0" side="bottom" align="start">
130
+ <PopoverContent className="w-[200px] p-0" side="bottom" align="start" onWheel={(e) => e.stopPropagation()}>
131
131
  {(showSearch === true || items.length > 10) && (
132
132
  <div className="p-2">
133
133
  <Input
@@ -34,7 +34,7 @@ export function NavigationConfirmation(props: Readonly<NavigationConfirmationPro
34
34
  return props.form.formState.isDirty;
35
35
  },
36
36
  withResolver: true,
37
- enableBeforeUnload: true,
37
+ enableBeforeUnload: () => props.form.formState.isDirty,
38
38
  });
39
39
  return (
40
40
  <Dialog open={status === 'blocked'} onOpenChange={reset}>