@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.
Files changed (67) hide show
  1. package/dist/plugin/vite-plugin-vendure-dashboard.js +1 -1
  2. package/package.json +40 -27
  3. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +32 -0
  4. package/src/app/routes/_authenticated/_collections/collections.tsx +153 -133
  5. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
  6. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +34 -1
  7. package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +430 -0
  8. package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +33 -0
  9. package/src/app/routes/_authenticated/_customers/components/customer-address-card.tsx +8 -3
  10. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -1
  11. package/src/app/routes/_authenticated/_products/products_.$id.tsx +1 -1
  12. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
  13. package/src/lib/components/data-input/money-input.tsx +2 -9
  14. package/src/lib/components/data-table/data-table.tsx +1 -1
  15. package/src/lib/components/shared/form-field-wrapper.tsx +22 -13
  16. package/src/lib/components/shared/paginated-list-data-table.tsx +1 -1
  17. package/src/lib/components/ui/accordion.tsx +50 -45
  18. package/src/lib/components/ui/alert-dialog.tsx +122 -93
  19. package/src/lib/components/ui/alert.tsx +54 -48
  20. package/src/lib/components/ui/aspect-ratio.tsx +9 -0
  21. package/src/lib/components/ui/avatar.tsx +53 -0
  22. package/src/lib/components/ui/badge.tsx +37 -29
  23. package/src/lib/components/ui/breadcrumb.tsx +89 -82
  24. package/src/lib/components/ui/button.tsx +52 -51
  25. package/src/lib/components/ui/calendar.tsx +196 -435
  26. package/src/lib/components/ui/card.tsx +78 -33
  27. package/src/lib/components/ui/carousel.tsx +241 -0
  28. package/src/lib/components/ui/chart.tsx +351 -0
  29. package/src/lib/components/ui/checkbox.tsx +28 -23
  30. package/src/lib/components/ui/collapsible.tsx +0 -2
  31. package/src/lib/components/ui/command.tsx +159 -114
  32. package/src/lib/components/ui/context-menu.tsx +252 -0
  33. package/src/lib/components/ui/dialog.tsx +115 -90
  34. package/src/lib/components/ui/drawer.tsx +133 -0
  35. package/src/lib/components/ui/dropdown-menu.tsx +207 -170
  36. package/src/lib/components/ui/form.tsx +138 -114
  37. package/src/lib/components/ui/hover-card.tsx +32 -26
  38. package/src/lib/components/ui/input-otp.tsx +77 -0
  39. package/src/lib/components/ui/input.tsx +17 -15
  40. package/src/lib/components/ui/label.tsx +19 -16
  41. package/src/lib/components/ui/menubar.tsx +274 -0
  42. package/src/lib/components/ui/navigation-menu.tsx +168 -0
  43. package/src/lib/components/ui/pagination.tsx +108 -87
  44. package/src/lib/components/ui/popover.tsx +36 -28
  45. package/src/lib/components/ui/progress.tsx +29 -0
  46. package/src/lib/components/ui/radio-group.tsx +45 -0
  47. package/src/lib/components/ui/resizable.tsx +54 -0
  48. package/src/lib/components/ui/scroll-area.tsx +48 -40
  49. package/src/lib/components/ui/select.tsx +151 -129
  50. package/src/lib/components/ui/separator.tsx +22 -20
  51. package/src/lib/components/ui/sheet.tsx +110 -91
  52. package/src/lib/components/ui/sidebar.tsx +652 -622
  53. package/src/lib/components/ui/skeleton.tsx +10 -10
  54. package/src/lib/components/ui/slider.tsx +63 -0
  55. package/src/lib/components/ui/sonner.tsx +7 -11
  56. package/src/lib/components/ui/switch.tsx +27 -22
  57. package/src/lib/components/ui/table.tsx +96 -64
  58. package/src/lib/components/ui/tabs.tsx +56 -38
  59. package/src/lib/components/ui/textarea.tsx +14 -14
  60. package/src/lib/components/ui/toggle-group.tsx +73 -0
  61. package/src/lib/components/ui/toggle.tsx +45 -0
  62. package/src/lib/components/ui/tooltip.tsx +45 -37
  63. package/src/lib/framework/component-registry/component-registry.tsx +5 -3
  64. package/src/lib/framework/page/detail-page.tsx +28 -17
  65. package/src/lib/framework/page/list-page.tsx +1 -1
  66. package/src/lib/index.ts +5 -6
  67. 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
- <EditIcon className="w-4 h-4" />
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
- <TrashIcon className="w-4 h-4 text-destructive" />
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/richt-text-input.js';
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/richt-text-input.js';
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/richt-text-input.js';
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
- FormControl,
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
- >({ control, name, render, label, description }: FormFieldWrapperProps<TFieldValues, TName>) {
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();