@vendure/dashboard 3.3.6-master-202507030648 → 3.3.6-master-202507030835
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/dist/plugin/vite-plugin-vendure-dashboard.js +1 -1
- package/package.json +40 -27
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +32 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +153 -133
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +34 -1
- package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +430 -0
- package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +33 -0
- package/src/app/routes/_authenticated/_customers/components/customer-address-card.tsx +8 -3
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
- package/src/lib/components/data-input/money-input.tsx +2 -9
- package/src/lib/components/data-table/data-table.tsx +1 -1
- package/src/lib/components/shared/form-field-wrapper.tsx +22 -13
- package/src/lib/components/shared/paginated-list-data-table.tsx +1 -1
- package/src/lib/components/ui/accordion.tsx +50 -45
- package/src/lib/components/ui/alert-dialog.tsx +122 -93
- package/src/lib/components/ui/alert.tsx +54 -48
- package/src/lib/components/ui/aspect-ratio.tsx +9 -0
- package/src/lib/components/ui/avatar.tsx +53 -0
- package/src/lib/components/ui/badge.tsx +37 -29
- package/src/lib/components/ui/breadcrumb.tsx +89 -82
- package/src/lib/components/ui/button.tsx +52 -51
- package/src/lib/components/ui/calendar.tsx +196 -435
- package/src/lib/components/ui/card.tsx +78 -33
- package/src/lib/components/ui/carousel.tsx +241 -0
- package/src/lib/components/ui/chart.tsx +351 -0
- package/src/lib/components/ui/checkbox.tsx +28 -23
- package/src/lib/components/ui/collapsible.tsx +0 -2
- package/src/lib/components/ui/command.tsx +159 -114
- package/src/lib/components/ui/context-menu.tsx +252 -0
- package/src/lib/components/ui/dialog.tsx +115 -90
- package/src/lib/components/ui/drawer.tsx +133 -0
- package/src/lib/components/ui/dropdown-menu.tsx +207 -170
- package/src/lib/components/ui/form.tsx +138 -114
- package/src/lib/components/ui/hover-card.tsx +32 -26
- package/src/lib/components/ui/input-otp.tsx +77 -0
- package/src/lib/components/ui/input.tsx +17 -15
- package/src/lib/components/ui/label.tsx +19 -16
- package/src/lib/components/ui/menubar.tsx +274 -0
- package/src/lib/components/ui/navigation-menu.tsx +168 -0
- package/src/lib/components/ui/pagination.tsx +108 -87
- package/src/lib/components/ui/popover.tsx +36 -28
- package/src/lib/components/ui/progress.tsx +29 -0
- package/src/lib/components/ui/radio-group.tsx +45 -0
- package/src/lib/components/ui/resizable.tsx +54 -0
- package/src/lib/components/ui/scroll-area.tsx +48 -40
- package/src/lib/components/ui/select.tsx +151 -129
- package/src/lib/components/ui/separator.tsx +22 -20
- package/src/lib/components/ui/sheet.tsx +110 -91
- package/src/lib/components/ui/sidebar.tsx +652 -622
- package/src/lib/components/ui/skeleton.tsx +10 -10
- package/src/lib/components/ui/slider.tsx +63 -0
- package/src/lib/components/ui/sonner.tsx +7 -11
- package/src/lib/components/ui/switch.tsx +27 -22
- package/src/lib/components/ui/table.tsx +96 -64
- package/src/lib/components/ui/tabs.tsx +56 -38
- package/src/lib/components/ui/textarea.tsx +14 -14
- package/src/lib/components/ui/toggle-group.tsx +73 -0
- package/src/lib/components/ui/toggle.tsx +45 -0
- package/src/lib/components/ui/tooltip.tsx +45 -37
- package/src/lib/framework/component-registry/component-registry.tsx +5 -3
- package/src/lib/framework/page/detail-page.tsx +28 -17
- package/src/lib/framework/page/list-page.tsx +1 -1
- package/src/lib/index.ts +5 -6
- package/vite/vite-plugin-vendure-dashboard.ts +1 -1
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { useDebounce } from '@uidotdev/usehooks';
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
import { Alert, AlertDescription } from '@/components/ui/alert.js';
|
|
7
|
+
import { Button } from '@/components/ui/button.js';
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
} from '@/components/ui/dialog.js';
|
|
16
|
+
import { Input } from '@/components/ui/input.js';
|
|
17
|
+
import { ScrollArea } from '@/components/ui/scroll-area.js';
|
|
18
|
+
import { api } from '@/graphql/api.js';
|
|
19
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
20
|
+
import { ChevronRight, Folder, FolderOpen, Search } from 'lucide-react';
|
|
21
|
+
|
|
22
|
+
import { collectionListForMoveDocument, moveCollectionDocument } from '../collections.graphql.js';
|
|
23
|
+
|
|
24
|
+
type Collection = {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
children?: { id: string }[] | null;
|
|
29
|
+
breadcrumbs: Array<{ id: string; name: string; slug: string }>;
|
|
30
|
+
parentId?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface MoveCollectionsDialogProps {
|
|
34
|
+
open: boolean;
|
|
35
|
+
onOpenChange: (open: boolean) => void;
|
|
36
|
+
collectionsToMove: Collection[];
|
|
37
|
+
onSuccess?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CollectionTreeNodeProps {
|
|
41
|
+
collection: Collection;
|
|
42
|
+
depth: number;
|
|
43
|
+
expanded: Record<string, boolean>;
|
|
44
|
+
onToggleExpanded: (id: string) => void;
|
|
45
|
+
onSelect: (collection: Collection) => void;
|
|
46
|
+
selectedCollectionId?: string;
|
|
47
|
+
collectionsToMove: Collection[];
|
|
48
|
+
childCollectionsByParentId: Record<string, Collection[]>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface TargetAlertProps {
|
|
52
|
+
selectedCollectionId?: string;
|
|
53
|
+
collectionsToMove: Collection[];
|
|
54
|
+
topLevelCollectionId?: string;
|
|
55
|
+
collectionNameCache: React.MutableRefObject<Map<string, string>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface MoveToTopLevelProps {
|
|
59
|
+
selectedCollectionId?: string;
|
|
60
|
+
topLevelCollectionId?: string;
|
|
61
|
+
onSelect: (id?: string) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function TargetAlert({
|
|
65
|
+
selectedCollectionId,
|
|
66
|
+
collectionsToMove,
|
|
67
|
+
topLevelCollectionId,
|
|
68
|
+
collectionNameCache,
|
|
69
|
+
}: Readonly<TargetAlertProps>) {
|
|
70
|
+
return (
|
|
71
|
+
<Alert className={selectedCollectionId ? 'border-blue-200 bg-blue-50' : ''}>
|
|
72
|
+
<Folder className="h-4 w-4" />
|
|
73
|
+
<AlertDescription>
|
|
74
|
+
{selectedCollectionId ? (
|
|
75
|
+
<Trans>
|
|
76
|
+
Moving {collectionsToMove.length} collection
|
|
77
|
+
{collectionsToMove.length === 1 ? '' : 's'} into{' '}
|
|
78
|
+
{selectedCollectionId === topLevelCollectionId
|
|
79
|
+
? 'top level'
|
|
80
|
+
: collectionNameCache.current.get(selectedCollectionId) || 'selected collection'}
|
|
81
|
+
</Trans>
|
|
82
|
+
) : (
|
|
83
|
+
<Trans>Select a destination collection</Trans>
|
|
84
|
+
)}
|
|
85
|
+
</AlertDescription>
|
|
86
|
+
</Alert>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function MoveToTopLevel({
|
|
91
|
+
selectedCollectionId,
|
|
92
|
+
topLevelCollectionId,
|
|
93
|
+
onSelect,
|
|
94
|
+
}: Readonly<MoveToTopLevelProps>) {
|
|
95
|
+
return (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className={`flex items-center gap-2 py-2 px-3 hover:bg-accent rounded-sm cursor-pointer w-full text-left ${
|
|
99
|
+
selectedCollectionId === topLevelCollectionId ? 'bg-accent' : ''
|
|
100
|
+
}`}
|
|
101
|
+
onClick={() => onSelect(topLevelCollectionId)}
|
|
102
|
+
>
|
|
103
|
+
<div className="w-3 h-3" />
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Folder className="h-4 w-4 text-muted-foreground" />
|
|
106
|
+
<span className="text-sm font-medium">
|
|
107
|
+
<Trans>Move to the top level</Trans>
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
</button>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function CollectionTreeNode({
|
|
115
|
+
collection,
|
|
116
|
+
depth,
|
|
117
|
+
expanded,
|
|
118
|
+
onToggleExpanded,
|
|
119
|
+
onSelect,
|
|
120
|
+
selectedCollectionId,
|
|
121
|
+
collectionsToMove,
|
|
122
|
+
childCollectionsByParentId,
|
|
123
|
+
}: Readonly<CollectionTreeNodeProps>) {
|
|
124
|
+
const hasChildren = collection.children && collection.children.length > 0;
|
|
125
|
+
const isExpanded = expanded[collection.id];
|
|
126
|
+
const isSelected = selectedCollectionId === collection.id;
|
|
127
|
+
const isBeingMoved = collectionsToMove.some(c => c.id === collection.id);
|
|
128
|
+
const isChildOfBeingMoved = collectionsToMove.some(c => collection.breadcrumbs.some(b => b.id === c.id));
|
|
129
|
+
|
|
130
|
+
// Don't allow selecting collections that are being moved or are children of collections being moved
|
|
131
|
+
const isSelectable = !isBeingMoved && !isChildOfBeingMoved;
|
|
132
|
+
|
|
133
|
+
const childCollections = childCollectionsByParentId[collection.id] || [];
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="my-0.5">
|
|
137
|
+
<div className="flex items-center" style={{ marginLeft: depth * 20 }}>
|
|
138
|
+
{hasChildren && (
|
|
139
|
+
<Button
|
|
140
|
+
size="icon"
|
|
141
|
+
variant="ghost"
|
|
142
|
+
className="h-4 w-4 p-0 mr-1"
|
|
143
|
+
onClick={() => onToggleExpanded(collection.id)}
|
|
144
|
+
>
|
|
145
|
+
{isExpanded ? (
|
|
146
|
+
<ChevronRight className="h-3 w-3 rotate-90" />
|
|
147
|
+
) : (
|
|
148
|
+
<ChevronRight className="h-3 w-3" />
|
|
149
|
+
)}
|
|
150
|
+
</Button>
|
|
151
|
+
)}
|
|
152
|
+
{!hasChildren && <div className="w-5 h-4 mr-1" />}
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className={`flex items-center gap-2 py-2 px-3 hover:bg-accent rounded-sm cursor-pointer w-full text-left ${
|
|
156
|
+
isSelected ? 'bg-accent' : ''
|
|
157
|
+
} ${!isSelectable ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
158
|
+
onClick={() => {
|
|
159
|
+
if (isSelectable) {
|
|
160
|
+
onSelect(collection);
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
disabled={!isSelectable}
|
|
164
|
+
>
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
{hasChildren &&
|
|
167
|
+
(isExpanded ? (
|
|
168
|
+
<FolderOpen className="h-4 w-4 text-muted-foreground" />
|
|
169
|
+
) : (
|
|
170
|
+
<Folder className="h-4 w-4 text-muted-foreground" />
|
|
171
|
+
))}
|
|
172
|
+
{!hasChildren && <div className="w-4 h-4" />}
|
|
173
|
+
<div className="flex flex-col">
|
|
174
|
+
<span className="text-sm">{collection.name}</span>
|
|
175
|
+
{collection.breadcrumbs.length > 1 && (
|
|
176
|
+
<span className="text-xs text-muted-foreground">
|
|
177
|
+
{collection.breadcrumbs
|
|
178
|
+
.slice(1)
|
|
179
|
+
.map(b => b.name)
|
|
180
|
+
.join(' / ')}
|
|
181
|
+
</span>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
{hasChildren && isExpanded && (
|
|
188
|
+
<div>
|
|
189
|
+
{childCollections.map((childCollection: Collection) => (
|
|
190
|
+
<CollectionTreeNode
|
|
191
|
+
key={childCollection.id}
|
|
192
|
+
collection={childCollection}
|
|
193
|
+
depth={depth + 1}
|
|
194
|
+
expanded={expanded}
|
|
195
|
+
onToggleExpanded={onToggleExpanded}
|
|
196
|
+
onSelect={onSelect}
|
|
197
|
+
selectedCollectionId={selectedCollectionId}
|
|
198
|
+
collectionsToMove={collectionsToMove}
|
|
199
|
+
childCollectionsByParentId={childCollectionsByParentId}
|
|
200
|
+
/>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function MoveCollectionsDialog({
|
|
209
|
+
open,
|
|
210
|
+
onOpenChange,
|
|
211
|
+
collectionsToMove,
|
|
212
|
+
onSuccess,
|
|
213
|
+
}: Readonly<MoveCollectionsDialogProps>) {
|
|
214
|
+
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
215
|
+
const [selectedCollectionId, setSelectedCollectionId] = useState<string>();
|
|
216
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
217
|
+
const debouncedSearchTerm = useDebounce(searchTerm, 300);
|
|
218
|
+
const collectionNameCache = useRef<Map<string, string>>(new Map());
|
|
219
|
+
const queryClient = useQueryClient();
|
|
220
|
+
const { i18n } = useLingui();
|
|
221
|
+
const collectionForMoveKey = ['collectionsForMove', debouncedSearchTerm];
|
|
222
|
+
const childCollectionsForMoveKey = (collectionId?: string) =>
|
|
223
|
+
collectionId ? ['childCollectionsForMove', collectionId] : ['childCollectionsForMove'];
|
|
224
|
+
|
|
225
|
+
const { data: collectionsData, isLoading } = useQuery({
|
|
226
|
+
queryKey: collectionForMoveKey,
|
|
227
|
+
queryFn: () =>
|
|
228
|
+
api.query(collectionListForMoveDocument, {
|
|
229
|
+
options: {
|
|
230
|
+
take: 100,
|
|
231
|
+
topLevelOnly: !debouncedSearchTerm,
|
|
232
|
+
...(debouncedSearchTerm && {
|
|
233
|
+
filter: {
|
|
234
|
+
name: { contains: debouncedSearchTerm },
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
staleTime: 1000 * 60 * 5,
|
|
240
|
+
enabled: open,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const topLevelCollectionId = collectionsData?.collections.items[0]?.parentId;
|
|
244
|
+
const selectionHasTopLevelParent = collectionsToMove.some(c => c.parentId === topLevelCollectionId);
|
|
245
|
+
|
|
246
|
+
// Load child collections for expanded nodes
|
|
247
|
+
const childrenQueries = useQueries({
|
|
248
|
+
queries: Object.entries(expanded).map(([collectionId, isExpanded]) => {
|
|
249
|
+
return {
|
|
250
|
+
queryKey: childCollectionsForMoveKey(collectionId),
|
|
251
|
+
queryFn: () =>
|
|
252
|
+
api.query(collectionListForMoveDocument, {
|
|
253
|
+
options: {
|
|
254
|
+
filter: {
|
|
255
|
+
parentId: { eq: collectionId },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
staleTime: 1000 * 60 * 5,
|
|
260
|
+
};
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const childCollectionsByParentId = childrenQueries.reduce(
|
|
265
|
+
(acc, query, index) => {
|
|
266
|
+
const collectionId = Object.keys(expanded)[index];
|
|
267
|
+
if (query.data) {
|
|
268
|
+
const collections = query.data.collections.items as Collection[];
|
|
269
|
+
// Populate the name cache with these collections
|
|
270
|
+
collections.forEach(collection => {
|
|
271
|
+
collectionNameCache.current.set(collection.id, collection.name);
|
|
272
|
+
});
|
|
273
|
+
acc[collectionId] = collections;
|
|
274
|
+
}
|
|
275
|
+
return acc;
|
|
276
|
+
},
|
|
277
|
+
{} as Record<string, Collection[]>,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const moveCollectionsMutation = useMutation({
|
|
281
|
+
mutationFn: api.mutate(moveCollectionDocument),
|
|
282
|
+
onSuccess: () => {
|
|
283
|
+
toast.success(i18n.t('Collections moved successfully'));
|
|
284
|
+
queryClient.invalidateQueries({ queryKey: collectionForMoveKey });
|
|
285
|
+
queryClient.invalidateQueries({ queryKey: childCollectionsForMoveKey() });
|
|
286
|
+
onSuccess?.();
|
|
287
|
+
onOpenChange(false);
|
|
288
|
+
},
|
|
289
|
+
onError: error => {
|
|
290
|
+
toast.error(i18n.t('Failed to move collections'));
|
|
291
|
+
console.error('Move collections error:', error);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const handleToggleExpanded = (id: string) => {
|
|
296
|
+
setExpanded(prev => ({
|
|
297
|
+
...prev,
|
|
298
|
+
[id]: !prev[id],
|
|
299
|
+
}));
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleSelect = (collection: Collection) => {
|
|
303
|
+
setSelectedCollectionId(collection.id);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const handleMove = () => {
|
|
307
|
+
if (!selectedCollectionId) {
|
|
308
|
+
toast.error(i18n.t('Please select a target collection'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Move to a specific parent using moveCollection
|
|
312
|
+
const movePromises = collectionsToMove.map((collection: Collection) =>
|
|
313
|
+
moveCollectionsMutation.mutateAsync({
|
|
314
|
+
input: {
|
|
315
|
+
collectionId: collection.id,
|
|
316
|
+
parentId: selectedCollectionId,
|
|
317
|
+
index: 0, // Move to the beginning of the target collection
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
Promise.all(movePromises);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const collections = (collectionsData?.collections.items as Collection[]) || [];
|
|
325
|
+
|
|
326
|
+
// Populate the name cache with top-level collections
|
|
327
|
+
collections.forEach(collection => {
|
|
328
|
+
collectionNameCache.current.set(collection.id, collection.name);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
333
|
+
<DialogContent className="sm:max-w-[600px] max-h-[80vh]">
|
|
334
|
+
<DialogHeader>
|
|
335
|
+
<DialogTitle>
|
|
336
|
+
<Trans>Move Collections</Trans>
|
|
337
|
+
</DialogTitle>
|
|
338
|
+
<DialogDescription>
|
|
339
|
+
<Trans>
|
|
340
|
+
Select a target collection to move{' '}
|
|
341
|
+
{collectionsToMove.length === 1
|
|
342
|
+
? 'this collection'
|
|
343
|
+
: `${collectionsToMove.length} collections`}{' '}
|
|
344
|
+
to.
|
|
345
|
+
</Trans>
|
|
346
|
+
</DialogDescription>
|
|
347
|
+
</DialogHeader>
|
|
348
|
+
<div className="px-6 py-3 bg-muted/50 border-b">
|
|
349
|
+
<div className="flex flex-wrap gap-2">
|
|
350
|
+
{collectionsToMove.map(collection => (
|
|
351
|
+
<div
|
|
352
|
+
key={collection.id}
|
|
353
|
+
className="flex items-center gap-2 px-3 py-1 bg-background border rounded-md text-sm"
|
|
354
|
+
>
|
|
355
|
+
<Folder className="h-3 w-3 text-muted-foreground" />
|
|
356
|
+
<span>{collection.name}</span>
|
|
357
|
+
</div>
|
|
358
|
+
))}
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
<div className="py-4">
|
|
362
|
+
<div className="px-6 pb-3">
|
|
363
|
+
<div className="relative mb-3">
|
|
364
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
365
|
+
<Input
|
|
366
|
+
placeholder={i18n.t('Filter by collection name')}
|
|
367
|
+
value={searchTerm}
|
|
368
|
+
onChange={e => setSearchTerm(e.target.value)}
|
|
369
|
+
className="pl-10"
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
<ScrollArea className="h-[400px]">
|
|
373
|
+
<div className="space-y-1">
|
|
374
|
+
{isLoading ? (
|
|
375
|
+
<div className="flex items-center justify-center py-8">
|
|
376
|
+
<Trans>Loading collections...</Trans>
|
|
377
|
+
</div>
|
|
378
|
+
) : (
|
|
379
|
+
<>
|
|
380
|
+
{!debouncedSearchTerm && !selectionHasTopLevelParent && (
|
|
381
|
+
<MoveToTopLevel
|
|
382
|
+
selectedCollectionId={selectedCollectionId}
|
|
383
|
+
topLevelCollectionId={topLevelCollectionId}
|
|
384
|
+
onSelect={setSelectedCollectionId}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
387
|
+
{collections.map((collection: Collection) => (
|
|
388
|
+
<CollectionTreeNode
|
|
389
|
+
key={collection.id}
|
|
390
|
+
collection={collection}
|
|
391
|
+
depth={0}
|
|
392
|
+
expanded={expanded}
|
|
393
|
+
onToggleExpanded={handleToggleExpanded}
|
|
394
|
+
onSelect={handleSelect}
|
|
395
|
+
selectedCollectionId={selectedCollectionId}
|
|
396
|
+
collectionsToMove={collectionsToMove}
|
|
397
|
+
childCollectionsByParentId={childCollectionsByParentId}
|
|
398
|
+
/>
|
|
399
|
+
))}
|
|
400
|
+
</>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
</ScrollArea>
|
|
404
|
+
<TargetAlert
|
|
405
|
+
selectedCollectionId={selectedCollectionId}
|
|
406
|
+
collectionsToMove={collectionsToMove}
|
|
407
|
+
topLevelCollectionId={topLevelCollectionId}
|
|
408
|
+
collectionNameCache={collectionNameCache}
|
|
409
|
+
/>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
<DialogFooter>
|
|
413
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
414
|
+
<Trans>Cancel</Trans>
|
|
415
|
+
</Button>
|
|
416
|
+
<Button
|
|
417
|
+
onClick={handleMove}
|
|
418
|
+
disabled={!selectedCollectionId || moveCollectionsMutation.isPending}
|
|
419
|
+
>
|
|
420
|
+
{moveCollectionsMutation.isPending ? (
|
|
421
|
+
<Trans>Moving...</Trans>
|
|
422
|
+
) : (
|
|
423
|
+
<Trans>Move Collections</Trans>
|
|
424
|
+
)}
|
|
425
|
+
</Button>
|
|
426
|
+
</DialogFooter>
|
|
427
|
+
</DialogContent>
|
|
428
|
+
</Dialog>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ResultOf } from 'gql.tada';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { collectionListDocument } from '../collections.graphql.js';
|
|
5
|
+
import { MoveCollectionsDialog } from './move-collections-dialog.js';
|
|
6
|
+
|
|
7
|
+
type Collection = ResultOf<typeof collectionListDocument>['collections']['items'][number];
|
|
8
|
+
|
|
9
|
+
export function useMoveSingleCollection() {
|
|
10
|
+
const [moveDialogOpen, setMoveDialogOpen] = useState(false);
|
|
11
|
+
const [collectionsToMove, setCollectionsToMove] = useState<Collection[]>([]);
|
|
12
|
+
|
|
13
|
+
const handleMoveClick = (collection: Collection) => {
|
|
14
|
+
setCollectionsToMove([collection]);
|
|
15
|
+
setMoveDialogOpen(true);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const MoveDialog = () => (
|
|
19
|
+
<MoveCollectionsDialog
|
|
20
|
+
open={moveDialogOpen}
|
|
21
|
+
onOpenChange={setMoveDialogOpen}
|
|
22
|
+
collectionsToMove={collectionsToMove}
|
|
23
|
+
onSuccess={() => {
|
|
24
|
+
// The dialog will handle invalidating queries internally
|
|
25
|
+
}}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
handleMoveClick,
|
|
31
|
+
MoveDialog,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DialogTrigger,
|
|
12
12
|
} from '@/vdb/components/ui/dialog.js';
|
|
13
13
|
import { api } from '@/vdb/graphql/api.js';
|
|
14
|
+
import { Button } from '@/vdb/index.js';
|
|
14
15
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
15
16
|
import { useMutation } from '@tanstack/react-query';
|
|
16
17
|
import { EditIcon, TrashIcon } from 'lucide-react';
|
|
@@ -120,8 +121,10 @@ export function CustomerAddressCard({
|
|
|
120
121
|
<div className="flex gap-4 mt-3 pt-3 border-t border-border">
|
|
121
122
|
{editable && (
|
|
122
123
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
123
|
-
<DialogTrigger>
|
|
124
|
-
<
|
|
124
|
+
<DialogTrigger asChild>
|
|
125
|
+
<Button size="icon" variant="secondary">
|
|
126
|
+
<EditIcon />
|
|
127
|
+
</Button>
|
|
125
128
|
</DialogTrigger>
|
|
126
129
|
<DialogContent>
|
|
127
130
|
<DialogHeader>
|
|
@@ -145,7 +148,9 @@ export function CustomerAddressCard({
|
|
|
145
148
|
onDelete?.();
|
|
146
149
|
}}
|
|
147
150
|
>
|
|
148
|
-
<
|
|
151
|
+
<Button size="icon" variant="destructive">
|
|
152
|
+
<TrashIcon />
|
|
153
|
+
</Button>
|
|
149
154
|
</ConfirmationDialog>
|
|
150
155
|
)}
|
|
151
156
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RichTextInput } from '@/vdb/components/data-input/
|
|
1
|
+
import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
|
|
2
2
|
import { ErrorPage } from '@/vdb/components/shared/error-page.js';
|
|
3
3
|
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
4
4
|
import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RichTextInput } from '@/vdb/components/data-input/
|
|
1
|
+
import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
|
|
2
2
|
import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
|
|
3
3
|
import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
|
|
4
4
|
import { ErrorPage } from '@/vdb/components/shared/error-page.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
|
|
2
|
-
import { RichTextInput } from '@/vdb/components/data-input/
|
|
2
|
+
import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
|
|
3
3
|
import { ErrorPage } from '@/vdb/components/shared/error-page.js';
|
|
4
4
|
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
5
5
|
import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
+
import { DataInputComponentProps } from '@/vdb/framework/component-registry/component-registry.js';
|
|
1
2
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
2
3
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
3
4
|
import { useEffect, useMemo, useState } from 'react';
|
|
4
5
|
import { AffixedInput } from './affixed-input.js';
|
|
5
6
|
|
|
6
7
|
// Original component
|
|
7
|
-
function MoneyInputInternal({
|
|
8
|
-
value,
|
|
9
|
-
currency,
|
|
10
|
-
onChange,
|
|
11
|
-
}: {
|
|
12
|
-
value: number;
|
|
13
|
-
currency: string;
|
|
14
|
-
onChange: (value: number) => void;
|
|
15
|
-
}) {
|
|
8
|
+
function MoneyInputInternal({ value, currency, onChange }: DataInputComponentProps) {
|
|
16
9
|
const {
|
|
17
10
|
settings: { displayLanguage, displayLocale },
|
|
18
11
|
} = useUserSettings();
|
|
@@ -81,7 +81,7 @@ export function DataTable<TData>({
|
|
|
81
81
|
bulkActions,
|
|
82
82
|
setTableOptions,
|
|
83
83
|
onRefresh,
|
|
84
|
-
}: DataTableProps<TData
|
|
84
|
+
}: Readonly<DataTableProps<TData>>) {
|
|
85
85
|
const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
|
|
86
86
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
|
|
87
87
|
const { activeChannel } = useChannel();
|
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
FormDescription,
|
|
4
|
-
FormItem,
|
|
5
|
-
FormLabel,
|
|
6
|
-
FormMessage,
|
|
7
|
-
FormField,
|
|
8
|
-
} from '../ui/form.js';
|
|
9
|
-
import { FieldValues, FieldPath } from 'react-hook-form';
|
|
1
|
+
import { FieldPath, FieldValues } from 'react-hook-form';
|
|
2
|
+
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../ui/form.js';
|
|
10
3
|
|
|
11
4
|
export type FormFieldWrapperProps<
|
|
12
5
|
TFieldValues extends FieldValues = FieldValues,
|
|
13
|
-
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues
|
|
6
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
14
7
|
> = React.ComponentProps<typeof FormField<TFieldValues, TName>> & {
|
|
15
8
|
label?: React.ReactNode;
|
|
16
9
|
description?: React.ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* @description
|
|
12
|
+
* Whether to render the form control.
|
|
13
|
+
* If false, the form control will not be rendered.
|
|
14
|
+
* This is useful when you want to render the form control in a custom way, e.g. for <Select/> components,
|
|
15
|
+
* where the FormControl needs to nested in the root component.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
renderFormControl?: boolean;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export function FormFieldWrapper<
|
|
20
22
|
TFieldValues extends FieldValues = FieldValues,
|
|
21
|
-
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues
|
|
22
|
-
>({
|
|
23
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
24
|
+
>({
|
|
25
|
+
control,
|
|
26
|
+
name,
|
|
27
|
+
render,
|
|
28
|
+
label,
|
|
29
|
+
description,
|
|
30
|
+
renderFormControl = true,
|
|
31
|
+
}: FormFieldWrapperProps<TFieldValues, TName>) {
|
|
23
32
|
return (
|
|
24
33
|
<FormField
|
|
25
34
|
control={control}
|
|
@@ -27,7 +36,7 @@ export function FormFieldWrapper<
|
|
|
27
36
|
render={renderArgs => (
|
|
28
37
|
<FormItem>
|
|
29
38
|
{label && <FormLabel>{label}</FormLabel>}
|
|
30
|
-
<FormControl>{render(renderArgs)}</FormControl>
|
|
39
|
+
{renderFormControl ? <FormControl>{render(renderArgs)}</FormControl> : render(renderArgs)}
|
|
31
40
|
{description && <FormDescription>{description}</FormDescription>}
|
|
32
41
|
<FormMessage />
|
|
33
42
|
</FormItem>
|
|
@@ -273,7 +273,7 @@ export function PaginatedListDataTable<
|
|
|
273
273
|
setTableOptions,
|
|
274
274
|
transformData,
|
|
275
275
|
registerRefresher,
|
|
276
|
-
}: PaginatedListDataTableProps<T, U, V, AC
|
|
276
|
+
}: Readonly<PaginatedListDataTableProps<T, U, V, AC>>) {
|
|
277
277
|
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
|
278
278
|
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
|
279
279
|
const queryClient = useQueryClient();
|