@vendure/dashboard 3.5.2-master-202512170238 → 3.5.2-master-202512180239
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/dashboard.plugin.js +1 -1
- package/package.json +3 -3
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +249 -167
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +8 -0
- package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +4 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +2 -9
- package/src/lib/components/data-input/number-input.tsx +24 -5
- package/src/lib/components/data-table/data-table-utils.ts +241 -1
- package/src/lib/components/data-table/data-table.tsx +189 -60
- package/src/lib/components/shared/paginated-list-data-table.tsx +19 -0
- package/src/lib/components/ui/alert.tsx +1 -1
- package/src/lib/framework/page/list-page.tsx +62 -38
- package/src/lib/hooks/use-drag-and-drop.ts +86 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessorFnColumnDef } from '@tanstack/react-table';
|
|
1
|
+
import { AccessorFnColumnDef, ExpandedState } from '@tanstack/react-table';
|
|
2
2
|
import { AccessorKeyColumnDef } from '@tanstack/table-core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -49,3 +49,243 @@ export function getStandardizedDefaultColumnOrder<T extends string | number | sy
|
|
|
49
49
|
const rest = defaultColumnOrder.filter(c => !standardFirstColumns.has(c as string));
|
|
50
50
|
return [...standardFirstColumns, ...rest] as T[];
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hierarchical item type with parent-child relationships
|
|
55
|
+
*/
|
|
56
|
+
export interface HierarchicalItem {
|
|
57
|
+
id: string;
|
|
58
|
+
parentId?: string | null;
|
|
59
|
+
breadcrumbs?: Array<{ id: string }>;
|
|
60
|
+
children?: Array<{ id: string }> | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Gets the parent ID of a hierarchical item
|
|
65
|
+
*/
|
|
66
|
+
export function getItemParentId<T extends HierarchicalItem>(
|
|
67
|
+
item: T | null | undefined,
|
|
68
|
+
): string | null | undefined {
|
|
69
|
+
return item?.parentId || item?.breadcrumbs?.[0]?.id;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gets all siblings (items with the same parent) for a given parent ID
|
|
74
|
+
*/
|
|
75
|
+
export function getItemSiblings<T extends HierarchicalItem>(
|
|
76
|
+
items: T[],
|
|
77
|
+
parentId: string | null | undefined,
|
|
78
|
+
): T[] {
|
|
79
|
+
return items.filter(item => getItemParentId(item) === parentId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks if moving an item to a new parent would create a circular reference
|
|
84
|
+
*/
|
|
85
|
+
export function isCircularReference<T extends HierarchicalItem>(
|
|
86
|
+
item: T,
|
|
87
|
+
targetParentId: string,
|
|
88
|
+
items: T[],
|
|
89
|
+
): boolean {
|
|
90
|
+
const targetParentItem = items.find(i => i.id === targetParentId);
|
|
91
|
+
return (
|
|
92
|
+
item.children?.some(child => {
|
|
93
|
+
if (child.id === targetParentId) return true;
|
|
94
|
+
const targetBreadcrumbIds = targetParentItem?.breadcrumbs?.map(b => b.id) || [];
|
|
95
|
+
return targetBreadcrumbIds.includes(item.id);
|
|
96
|
+
}) ?? false
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Result of calculating the target position for a drag and drop operation
|
|
102
|
+
*/
|
|
103
|
+
export interface TargetPosition {
|
|
104
|
+
targetParentId: string;
|
|
105
|
+
adjustedIndex: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Context for drag and drop position calculation
|
|
110
|
+
*/
|
|
111
|
+
interface DragContext<T extends HierarchicalItem> {
|
|
112
|
+
item: T;
|
|
113
|
+
targetItem: T | undefined;
|
|
114
|
+
previousItem: T | null;
|
|
115
|
+
isDraggingDown: boolean;
|
|
116
|
+
isTargetExpanded: boolean;
|
|
117
|
+
isPreviousExpanded: boolean;
|
|
118
|
+
sourceParentId: string;
|
|
119
|
+
items: T[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Checks if dragging down directly onto an expanded item
|
|
124
|
+
*/
|
|
125
|
+
function isDroppingIntoExpandedTarget<T extends HierarchicalItem>(context: DragContext<T>): boolean {
|
|
126
|
+
const { isDraggingDown, targetItem, item, isTargetExpanded } = context;
|
|
127
|
+
return isDraggingDown && targetItem?.id !== item.id && isTargetExpanded;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Checks if dragging down into an expanded item's children area
|
|
132
|
+
*/
|
|
133
|
+
function isDroppingIntoExpandedPreviousChildren<T extends HierarchicalItem>(
|
|
134
|
+
context: DragContext<T>,
|
|
135
|
+
): boolean {
|
|
136
|
+
const { isDraggingDown, targetItem, previousItem, item, isPreviousExpanded } = context;
|
|
137
|
+
return (
|
|
138
|
+
isDraggingDown &&
|
|
139
|
+
previousItem !== null &&
|
|
140
|
+
targetItem?.id !== item.id &&
|
|
141
|
+
isPreviousExpanded &&
|
|
142
|
+
targetItem?.parentId === previousItem.id
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Checks if dragging up into an expanded item's children area
|
|
148
|
+
*/
|
|
149
|
+
function isDroppingIntoExpandedPreviousWhenDraggingUp<T extends HierarchicalItem>(
|
|
150
|
+
context: DragContext<T>,
|
|
151
|
+
): boolean {
|
|
152
|
+
const { isDraggingDown, previousItem, isPreviousExpanded } = context;
|
|
153
|
+
return !isDraggingDown && previousItem !== null && isPreviousExpanded;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates a position for dropping into an expanded item as first child
|
|
158
|
+
*/
|
|
159
|
+
function createFirstChildPosition(parentId: string): TargetPosition {
|
|
160
|
+
return { targetParentId: parentId, adjustedIndex: 0 };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Calculates position for cross-parent drag operations
|
|
165
|
+
*/
|
|
166
|
+
function calculateCrossParentPosition<T extends HierarchicalItem>(
|
|
167
|
+
targetItem: T,
|
|
168
|
+
sourceParentId: string,
|
|
169
|
+
items: T[],
|
|
170
|
+
): TargetPosition | null {
|
|
171
|
+
const targetItemParentId = getItemParentId(targetItem);
|
|
172
|
+
|
|
173
|
+
if (!targetItemParentId || targetItemParentId === sourceParentId) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const targetSiblings = getItemSiblings(items, targetItemParentId);
|
|
178
|
+
const adjustedIndex = targetSiblings.findIndex(i => i.id === targetItem.id);
|
|
179
|
+
|
|
180
|
+
return { targetParentId: targetItemParentId, adjustedIndex };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Calculates position when dropping at the end of the list
|
|
185
|
+
*/
|
|
186
|
+
function calculateDropAtEndPosition<T extends HierarchicalItem>(
|
|
187
|
+
previousItem: T | null,
|
|
188
|
+
sourceParentId: string,
|
|
189
|
+
items: T[],
|
|
190
|
+
): TargetPosition | null {
|
|
191
|
+
if (!previousItem) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const previousItemParentId = getItemParentId(previousItem);
|
|
196
|
+
|
|
197
|
+
if (!previousItemParentId || previousItemParentId === sourceParentId) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const targetSiblings = getItemSiblings(items, previousItemParentId);
|
|
202
|
+
return { targetParentId: previousItemParentId, adjustedIndex: targetSiblings.length };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Determines the target parent and index for a hierarchical drag and drop operation
|
|
207
|
+
*/
|
|
208
|
+
export function calculateDragTargetPosition<T extends HierarchicalItem>(params: {
|
|
209
|
+
item: T;
|
|
210
|
+
oldIndex: number;
|
|
211
|
+
newIndex: number;
|
|
212
|
+
items: T[];
|
|
213
|
+
sourceParentId: string;
|
|
214
|
+
expanded: ExpandedState;
|
|
215
|
+
}): TargetPosition {
|
|
216
|
+
const { item, oldIndex, newIndex, items, sourceParentId, expanded } = params;
|
|
217
|
+
|
|
218
|
+
const targetItem = items[newIndex];
|
|
219
|
+
const previousItem = newIndex > 0 ? items[newIndex - 1] : null;
|
|
220
|
+
|
|
221
|
+
const context: DragContext<T> = {
|
|
222
|
+
item,
|
|
223
|
+
targetItem,
|
|
224
|
+
previousItem,
|
|
225
|
+
isDraggingDown: oldIndex < newIndex,
|
|
226
|
+
isTargetExpanded: targetItem ? !!expanded[targetItem.id as keyof ExpandedState] : false,
|
|
227
|
+
isPreviousExpanded: previousItem ? !!expanded[previousItem.id as keyof ExpandedState] : false,
|
|
228
|
+
sourceParentId,
|
|
229
|
+
items,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Handle dropping into expanded items (becomes first child)
|
|
233
|
+
if (isDroppingIntoExpandedTarget(context)) {
|
|
234
|
+
return createFirstChildPosition(targetItem.id);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (previousItem && isDroppingIntoExpandedPreviousChildren(context)) {
|
|
238
|
+
return createFirstChildPosition(previousItem.id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (previousItem && isDroppingIntoExpandedPreviousWhenDraggingUp(context)) {
|
|
242
|
+
return createFirstChildPosition(previousItem.id);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Handle cross-parent drag operations
|
|
246
|
+
if (targetItem?.id !== item.id) {
|
|
247
|
+
const crossParentPosition = calculateCrossParentPosition(targetItem, sourceParentId, items);
|
|
248
|
+
if (crossParentPosition) {
|
|
249
|
+
return crossParentPosition;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handle dropping at the end of the list
|
|
254
|
+
if (!targetItem && previousItem) {
|
|
255
|
+
const dropAtEndPosition = calculateDropAtEndPosition(previousItem, sourceParentId, items);
|
|
256
|
+
if (dropAtEndPosition) {
|
|
257
|
+
return dropAtEndPosition;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Default: stay in the same parent at the beginning
|
|
262
|
+
return { targetParentId: sourceParentId, adjustedIndex: 0 };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Calculates the adjusted sibling index when reordering within the same parent
|
|
267
|
+
*/
|
|
268
|
+
export function calculateSiblingIndex<T extends HierarchicalItem>(params: {
|
|
269
|
+
item: T;
|
|
270
|
+
oldIndex: number;
|
|
271
|
+
newIndex: number;
|
|
272
|
+
items: T[];
|
|
273
|
+
parentId: string;
|
|
274
|
+
}): number {
|
|
275
|
+
const { item, oldIndex, newIndex, items, parentId } = params;
|
|
276
|
+
|
|
277
|
+
const siblings = getItemSiblings(items, parentId);
|
|
278
|
+
const oldSiblingIndex = siblings.findIndex(i => i.id === item.id);
|
|
279
|
+
const isDraggingDown = oldIndex < newIndex;
|
|
280
|
+
|
|
281
|
+
let newSiblingIndex = oldSiblingIndex;
|
|
282
|
+
const [start, end] = isDraggingDown ? [oldIndex + 1, newIndex] : [newIndex, oldIndex - 1];
|
|
283
|
+
|
|
284
|
+
for (let i = start; i <= end; i++) {
|
|
285
|
+
if (getItemParentId(items[i]) === parentId) {
|
|
286
|
+
newSiblingIndex += isDraggingDown ? 1 : -1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return newSiblingIndex;
|
|
291
|
+
}
|
|
@@ -15,6 +15,17 @@ import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
|
15
15
|
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
16
16
|
import { useSavedViews } from '@/vdb/hooks/use-saved-views.js';
|
|
17
17
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
18
|
+
import {
|
|
19
|
+
closestCenter,
|
|
20
|
+
DndContext,
|
|
21
|
+
} from '@dnd-kit/core';
|
|
22
|
+
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
|
23
|
+
import {
|
|
24
|
+
SortableContext,
|
|
25
|
+
useSortable,
|
|
26
|
+
verticalListSortingStrategy,
|
|
27
|
+
} from '@dnd-kit/sortable';
|
|
28
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
18
29
|
import {
|
|
19
30
|
ColumnDef,
|
|
20
31
|
ColumnFilter,
|
|
@@ -23,18 +34,66 @@ import {
|
|
|
23
34
|
getCoreRowModel,
|
|
24
35
|
getPaginationRowModel,
|
|
25
36
|
PaginationState,
|
|
37
|
+
Row,
|
|
26
38
|
SortingState,
|
|
27
39
|
Table as TableType,
|
|
28
40
|
useReactTable,
|
|
29
41
|
VisibilityState,
|
|
30
42
|
} from '@tanstack/react-table';
|
|
31
43
|
import { RowSelectionState, TableOptions } from '@tanstack/table-core';
|
|
32
|
-
import
|
|
44
|
+
import { GripVertical } from 'lucide-react';
|
|
45
|
+
import React, { Suspense, useEffect, useId, useMemo, useRef } from 'react';
|
|
33
46
|
import { AddFilterMenu } from './add-filter-menu.js';
|
|
34
47
|
import { DataTableBulkActions } from './data-table-bulk-actions.js';
|
|
35
48
|
import { DataTableProvider } from './data-table-context.js';
|
|
36
49
|
import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
|
|
37
50
|
import { DataTableFilterBadgeEditable } from './data-table-filter-badge-editable.js';
|
|
51
|
+
import { useDragAndDrop } from '@/vdb/hooks/use-drag-and-drop.js';
|
|
52
|
+
import { toast } from 'sonner';
|
|
53
|
+
|
|
54
|
+
interface DraggableRowProps<TData> {
|
|
55
|
+
row: Row<TData>;
|
|
56
|
+
isDragDisabled: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function DraggableRow<TData>({ row, isDragDisabled }: Readonly<DraggableRowProps<TData>>) {
|
|
60
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
61
|
+
id: row.id,
|
|
62
|
+
disabled: isDragDisabled,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const style = {
|
|
66
|
+
transform: CSS.Transform.toString(transform),
|
|
67
|
+
transition,
|
|
68
|
+
opacity: isDragging ? 0.5 : 1,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<TableRow
|
|
73
|
+
ref={setNodeRef}
|
|
74
|
+
style={style}
|
|
75
|
+
data-state={row.getIsSelected() && 'selected'}
|
|
76
|
+
className="animate-in fade-in duration-100"
|
|
77
|
+
>
|
|
78
|
+
{!isDragDisabled && (
|
|
79
|
+
<TableCell className="w-[40px] h-12">
|
|
80
|
+
<div
|
|
81
|
+
{...attributes}
|
|
82
|
+
{...listeners}
|
|
83
|
+
className="cursor-move text-muted-foreground hover:text-foreground transition-colors"
|
|
84
|
+
>
|
|
85
|
+
<GripVertical className="h-4 w-4" />
|
|
86
|
+
</div>
|
|
87
|
+
</TableCell>
|
|
88
|
+
)}
|
|
89
|
+
{row.getVisibleCells().filter(cell => cell.column.id !== '__drag_handle__').map(cell => (
|
|
90
|
+
<TableCell key={cell.id} className="h-12">
|
|
91
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
92
|
+
</TableCell>
|
|
93
|
+
))}
|
|
94
|
+
</TableRow>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
38
97
|
|
|
39
98
|
export interface FacetedFilter {
|
|
40
99
|
title: string;
|
|
@@ -77,6 +136,18 @@ interface DataTableProps<TData> {
|
|
|
77
136
|
*/
|
|
78
137
|
setTableOptions?: (table: TableOptions<TData>) => TableOptions<TData>;
|
|
79
138
|
onRefresh?: () => void;
|
|
139
|
+
/**
|
|
140
|
+
* @description
|
|
141
|
+
* Callback when items are reordered via drag and drop.
|
|
142
|
+
* When provided, enables drag-and-drop functionality.
|
|
143
|
+
* The fourth parameter provides all items for context-aware reordering.
|
|
144
|
+
*/
|
|
145
|
+
onReorder?: (oldIndex: number, newIndex: number, item: TData, allItems?: TData[]) => void | Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* @description
|
|
148
|
+
* When true, drag and drop will be disabled. This will only have an effect if the onReorder prop is also set
|
|
149
|
+
*/
|
|
150
|
+
disableDragAndDrop?: boolean;
|
|
80
151
|
}
|
|
81
152
|
|
|
82
153
|
/**
|
|
@@ -111,6 +182,8 @@ export function DataTable<TData>({
|
|
|
111
182
|
bulkActions,
|
|
112
183
|
setTableOptions,
|
|
113
184
|
onRefresh,
|
|
185
|
+
onReorder,
|
|
186
|
+
disableDragAndDrop = false,
|
|
114
187
|
}: Readonly<DataTableProps<TData>>) {
|
|
115
188
|
const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
|
|
116
189
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
|
|
@@ -131,6 +204,16 @@ export function DataTable<TData>({
|
|
|
131
204
|
const prevSearchTermRef = useRef(searchTerm);
|
|
132
205
|
const prevColumnFiltersRef = useRef(columnFilters);
|
|
133
206
|
|
|
207
|
+
const componentId = useId();
|
|
208
|
+
const { sensors, localData, handleDragEnd, itemIds } = useDragAndDrop({
|
|
209
|
+
data,
|
|
210
|
+
onReorder,
|
|
211
|
+
disabled: disableDragAndDrop,
|
|
212
|
+
onError: error => {
|
|
213
|
+
toast.error(t`Failed to reorder items`);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
134
217
|
useEffect(() => {
|
|
135
218
|
// If the defaultColumnVisibility changes externally (e.g. the user reset the table settings),
|
|
136
219
|
// we want to reset the column visibility to the default.
|
|
@@ -143,9 +226,25 @@ export function DataTable<TData>({
|
|
|
143
226
|
// We intentionally do not include `columnVisibility` in the dependency array
|
|
144
227
|
}, [defaultColumnVisibility]);
|
|
145
228
|
|
|
229
|
+
// Add drag handle column if drag and drop is enabled
|
|
230
|
+
const columnsWithOptionalDragHandle = useMemo(() => {
|
|
231
|
+
if (!disableDragAndDrop && onReorder) {
|
|
232
|
+
const dragHandleColumn: ColumnDef<TData, any> = {
|
|
233
|
+
id: '__drag_handle__',
|
|
234
|
+
header: '',
|
|
235
|
+
cell: () => null, // Rendered by DraggableRow
|
|
236
|
+
size: 40,
|
|
237
|
+
enableSorting: false,
|
|
238
|
+
enableHiding: false,
|
|
239
|
+
};
|
|
240
|
+
return [dragHandleColumn, ...columns];
|
|
241
|
+
}
|
|
242
|
+
return columns;
|
|
243
|
+
}, [columns, disableDragAndDrop, onReorder]);
|
|
244
|
+
|
|
146
245
|
let tableOptions: TableOptions<TData> = {
|
|
147
|
-
data,
|
|
148
|
-
columns,
|
|
246
|
+
data: localData,
|
|
247
|
+
columns: columnsWithOptionalDragHandle,
|
|
149
248
|
getRowId: row => (row as { id: string }).id,
|
|
150
249
|
getCoreRowModel: getCoreRowModel(),
|
|
151
250
|
getPaginationRowModel: getPaginationRowModel(),
|
|
@@ -220,6 +319,8 @@ export function DataTable<TData>({
|
|
|
220
319
|
|
|
221
320
|
const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
|
|
222
321
|
|
|
322
|
+
const isDragDisabled = disableDragAndDrop || !onReorder;
|
|
323
|
+
|
|
223
324
|
return (
|
|
224
325
|
<DataTableProvider
|
|
225
326
|
columnFilters={columnFilters}
|
|
@@ -310,66 +411,94 @@ export function DataTable<TData>({
|
|
|
310
411
|
) : null}
|
|
311
412
|
|
|
312
413
|
<div className="rounded-md border my-2 relative shadow-sm">
|
|
313
|
-
<
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
414
|
+
<DndContext
|
|
415
|
+
sensors={sensors}
|
|
416
|
+
collisionDetection={closestCenter}
|
|
417
|
+
onDragEnd={handleDragEnd}
|
|
418
|
+
modifiers={[restrictToVerticalAxis]}
|
|
419
|
+
>
|
|
420
|
+
<Table>
|
|
421
|
+
<TableHeader className="bg-muted/50">
|
|
422
|
+
{table.getHeaderGroups().map(headerGroup => (
|
|
423
|
+
<TableRow key={headerGroup.id}>
|
|
424
|
+
{headerGroup.headers.map(header => {
|
|
425
|
+
return (
|
|
426
|
+
<TableHead key={header.id}>
|
|
427
|
+
{header.isPlaceholder
|
|
428
|
+
? null
|
|
429
|
+
: flexRender(
|
|
430
|
+
header.column.columnDef.header,
|
|
431
|
+
header.getContext(),
|
|
432
|
+
)}
|
|
433
|
+
</TableHead>
|
|
434
|
+
);
|
|
435
|
+
})}
|
|
436
|
+
</TableRow>
|
|
437
|
+
))}
|
|
438
|
+
</TableHeader>
|
|
439
|
+
<SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
|
|
440
|
+
<TableBody>
|
|
441
|
+
{isLoading && !localData?.length ? (
|
|
442
|
+
Array.from({ length: Math.min(pagination.pageSize, 100) }).map((_, index) => (
|
|
443
|
+
<TableRow
|
|
444
|
+
key={`skeleton-${index}`}
|
|
445
|
+
className="animate-in fade-in duration-100"
|
|
446
|
+
>
|
|
447
|
+
{!isDragDisabled && (
|
|
448
|
+
<TableCell className="w-[40px] h-12">
|
|
449
|
+
<Skeleton className="h-4 w-4" />
|
|
450
|
+
</TableCell>
|
|
451
|
+
)}
|
|
452
|
+
{Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
|
|
453
|
+
<TableCell
|
|
454
|
+
key={`skeleton-cell-${index}-${cellIndex}`}
|
|
455
|
+
className="h-12"
|
|
456
|
+
>
|
|
457
|
+
<Skeleton className="h-4 my-2 w-full" />
|
|
458
|
+
</TableCell>
|
|
459
|
+
))}
|
|
460
|
+
</TableRow>
|
|
461
|
+
))
|
|
462
|
+
) : table.getRowModel().rows?.length ? (
|
|
463
|
+
(() => {
|
|
464
|
+
const isDraggableEnabled = onReorder && !isDragDisabled;
|
|
465
|
+
const rows = table.getRowModel().rows;
|
|
466
|
+
|
|
467
|
+
if (isDraggableEnabled) {
|
|
468
|
+
return rows.map(row => (
|
|
469
|
+
<DraggableRow key={`${row.id}-${componentId}`} row={row} isDragDisabled={isDragDisabled} />
|
|
470
|
+
));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return rows.map(row => (
|
|
474
|
+
<TableRow
|
|
475
|
+
key={row.id}
|
|
476
|
+
data-state={row.getIsSelected() && 'selected'}
|
|
477
|
+
className="animate-in fade-in duration-100"
|
|
478
|
+
>
|
|
479
|
+
{row.getVisibleCells().map(cell => (
|
|
480
|
+
<TableCell key={cell.id} className="h-12">
|
|
481
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
482
|
+
</TableCell>
|
|
483
|
+
))}
|
|
484
|
+
</TableRow>
|
|
485
|
+
));
|
|
486
|
+
})()
|
|
487
|
+
) : (
|
|
488
|
+
<TableRow className="animate-in fade-in duration-100">
|
|
340
489
|
<TableCell
|
|
341
|
-
|
|
342
|
-
className="h-
|
|
490
|
+
colSpan={columnsWithOptionalDragHandle.length + (isDragDisabled ? 0 : 1)}
|
|
491
|
+
className="h-24 text-center"
|
|
343
492
|
>
|
|
344
|
-
<
|
|
493
|
+
<Trans>No results</Trans>
|
|
345
494
|
</TableCell>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
data-state={row.getIsSelected() && 'selected'}
|
|
354
|
-
className="animate-in fade-in duration-100"
|
|
355
|
-
>
|
|
356
|
-
{row.getVisibleCells().map(cell => (
|
|
357
|
-
<TableCell key={cell.id} className="h-12">
|
|
358
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
359
|
-
</TableCell>
|
|
360
|
-
))}
|
|
361
|
-
</TableRow>
|
|
362
|
-
))
|
|
363
|
-
) : (
|
|
364
|
-
<TableRow className="animate-in fade-in duration-100">
|
|
365
|
-
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
366
|
-
<Trans>No results</Trans>
|
|
367
|
-
</TableCell>
|
|
368
|
-
</TableRow>
|
|
369
|
-
)}
|
|
370
|
-
{children}
|
|
371
|
-
</TableBody>
|
|
372
|
-
</Table>
|
|
495
|
+
</TableRow>
|
|
496
|
+
)}
|
|
497
|
+
{children}
|
|
498
|
+
</TableBody>
|
|
499
|
+
</SortableContext>
|
|
500
|
+
</Table>
|
|
501
|
+
</DndContext>
|
|
373
502
|
<DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
|
|
374
503
|
</div>
|
|
375
504
|
{onPageChange && totalItems != null && <DataTablePagination table={table} />}
|
|
@@ -234,6 +234,21 @@ export interface PaginatedListDataTableProps<
|
|
|
234
234
|
* the list needs to be refreshed.
|
|
235
235
|
*/
|
|
236
236
|
registerRefresher?: PaginatedListRefresherRegisterFn;
|
|
237
|
+
/**
|
|
238
|
+
* @description
|
|
239
|
+
* Callback when items are reordered via drag and drop.
|
|
240
|
+
* When provided, enables drag-and-drop functionality.
|
|
241
|
+
*/
|
|
242
|
+
onReorder?: (
|
|
243
|
+
oldIndex: number,
|
|
244
|
+
newIndex: number,
|
|
245
|
+
item: PaginatedListItemFields<T>,
|
|
246
|
+
) => void | Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* @description
|
|
249
|
+
* When true, drag and drop will be disabled. This will only have an effect if the onReorder prop is also set
|
|
250
|
+
*/
|
|
251
|
+
disableDragAndDrop?: boolean;
|
|
237
252
|
}
|
|
238
253
|
|
|
239
254
|
export const PaginatedListDataTableKey = 'PaginatedListDataTable';
|
|
@@ -378,6 +393,8 @@ export function PaginatedListDataTable<
|
|
|
378
393
|
setTableOptions,
|
|
379
394
|
transformData,
|
|
380
395
|
registerRefresher,
|
|
396
|
+
onReorder,
|
|
397
|
+
disableDragAndDrop = false,
|
|
381
398
|
}: Readonly<PaginatedListDataTableProps<T, U, V, AC>>) {
|
|
382
399
|
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
|
383
400
|
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
|
@@ -498,6 +515,8 @@ export function PaginatedListDataTable<
|
|
|
498
515
|
bulkActions={bulkActions}
|
|
499
516
|
setTableOptions={setTableOptions}
|
|
500
517
|
onRefresh={refetchPaginatedList}
|
|
518
|
+
onReorder={onReorder}
|
|
519
|
+
disableDragAndDrop={disableDragAndDrop}
|
|
501
520
|
/>
|
|
502
521
|
</PaginatedListContext.Provider>
|
|
503
522
|
);
|
|
@@ -8,7 +8,7 @@ const alertVariants = cva(
|
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: 'bg-background text-
|
|
11
|
+
default: 'bg-background text-primary/80',
|
|
12
12
|
destructive:
|
|
13
13
|
'border-destructive/50 text-destructive dark:text-destructive-foreground/80 dark:border-destructive [&>svg]:text-current dark:bg-destructive/50',
|
|
14
14
|
},
|