@startsimpli/ui 0.1.0 → 0.1.3
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/chunk-27YUQBOE.mjs +3954 -0
- package/dist/chunk-27YUQBOE.mjs.map +1 -0
- package/dist/chunk-G2AM3DBU.mjs +1026 -0
- package/dist/chunk-G2AM3DBU.mjs.map +1 -0
- package/dist/chunk-G4XBXCFH.mjs +63 -0
- package/dist/chunk-G4XBXCFH.mjs.map +1 -0
- package/dist/chunk-LZOMFHX3.mjs +35 -0
- package/dist/chunk-LZOMFHX3.mjs.map +1 -0
- package/dist/chunk-QYXFLOO7.mjs +210 -0
- package/dist/chunk-QYXFLOO7.mjs.map +1 -0
- package/dist/components/index.d.mts +472 -0
- package/dist/components/index.d.ts +472 -0
- package/dist/components/index.js +5149 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +6 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/components/unified-table/index.d.mts +725 -0
- package/dist/components/unified-table/index.d.ts +725 -0
- package/dist/components/unified-table/index.js +4000 -0
- package/dist/components/unified-table/index.js.map +1 -0
- package/dist/components/unified-table/index.mjs +5 -0
- package/dist/components/unified-table/index.mjs.map +1 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +5448 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -0
- package/dist/theme/index.d.mts +20 -0
- package/dist/theme/index.d.ts +20 -0
- package/dist/theme/index.js +245 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/index.mjs +9 -0
- package/dist/theme/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +38 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +72 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +4 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +62 -21
- package/src/__mocks__/next/navigation.js +18 -0
- package/src/components/__tests__/safe-html.test.tsx +45 -0
- package/src/components/__tests__/states.test.tsx +94 -0
- package/src/components/__tests__/status-badge.test.tsx +101 -0
- package/src/components/__tests__/toast.test.tsx +124 -0
- package/src/components/badge/StatusBadge.tsx +55 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/dialog/BaseDialog.tsx +184 -0
- package/src/components/dialog/index.ts +8 -0
- package/src/components/index.ts +25 -0
- package/src/components/loading/DashboardSkeleton.tsx +27 -0
- package/src/components/loading/TableSkeleton.tsx +63 -0
- package/src/components/loading/index.ts +4 -0
- package/src/components/safe-html.tsx +18 -0
- package/src/components/states/EmptyState.tsx +48 -0
- package/src/components/states/ErrorState.tsx +76 -0
- package/src/components/states/index.ts +4 -0
- package/src/components/toast/Toaster.tsx +72 -0
- package/src/components/toast/index.ts +5 -0
- package/src/components/toast/use-notify.ts +45 -0
- package/src/components/toast/use-toast.ts +150 -0
- package/src/components/ui/api-error-boundary.tsx +64 -0
- package/src/components/ui/feature-gate.tsx +87 -0
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/page-loader.tsx +31 -0
- package/src/components/ui/query-provider.tsx +30 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +1 -1
- package/src/components/unified-table/hooks/useFilters.ts +1 -0
- package/src/components/unified-table/hooks/usePagination.ts +1 -0
- package/src/components/unified-table/hooks/useSelection.ts +2 -1
- package/src/components/unified-table/hooks/useTableKeyboard.ts +2 -1
- package/src/components/unified-table/hooks/useTablePreferences.ts +1 -0
- package/src/components/unified-table/hooks/useTableState.ts +1 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +1 -1
- package/src/components/unified-table/index.ts +4 -0
- package/src/components/wizard/StepIndicator.tsx +60 -0
- package/src/components/wizard/index.ts +2 -0
- package/src/theme/tailwind.config.d.ts +3 -0
- package/tailwind.preset.js +87 -0
|
@@ -0,0 +1,3954 @@
|
|
|
1
|
+
import { cn } from './chunk-G4XBXCFH.mjs';
|
|
2
|
+
import * as React5 from 'react';
|
|
3
|
+
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
|
4
|
+
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
|
5
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
6
|
+
import { cva } from 'class-variance-authority';
|
|
7
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
|
+
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
9
|
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
10
|
+
import { ChevronDown, ChevronUp, Check, ChevronRight, Circle, X, Columns3, Loader2, MoreVertical, Download, FileText, FileSpreadsheet, BookmarkPlus, Star, Trash2, Filter, XCircle, MoreHorizontal, ChevronsLeft, ChevronLeft, ChevronsRight, GripVertical, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
|
|
11
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
12
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
13
|
+
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
|
14
|
+
import * as XLSX from 'xlsx';
|
|
15
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
16
|
+
|
|
17
|
+
function useTableState({
|
|
18
|
+
initialData = [],
|
|
19
|
+
initialPageSize = 25
|
|
20
|
+
}) {
|
|
21
|
+
const [data, setDataState] = useState(initialData);
|
|
22
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
23
|
+
const [pageSize, setPageSizeState] = useState(initialPageSize);
|
|
24
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
25
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
|
26
|
+
const [sortBy, setSortBy] = useState(null);
|
|
27
|
+
const [sortDirection, setSortDirection] = useState("asc");
|
|
28
|
+
const [loading, setLoadingState] = useState(false);
|
|
29
|
+
const [loadingRows, setLoadingRowsState] = useState(/* @__PURE__ */ new Set());
|
|
30
|
+
const [error, setErrorState] = useState(null);
|
|
31
|
+
const [viewMode, setViewModeState] = useState("table");
|
|
32
|
+
const totalCount = data.length;
|
|
33
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
34
|
+
const state = useMemo(() => ({
|
|
35
|
+
data,
|
|
36
|
+
filteredData: data,
|
|
37
|
+
// Will be computed by filters hook
|
|
38
|
+
displayedData: data,
|
|
39
|
+
// Will be computed by pagination
|
|
40
|
+
currentPage,
|
|
41
|
+
pageSize,
|
|
42
|
+
totalPages,
|
|
43
|
+
totalCount,
|
|
44
|
+
selectedIds: /* @__PURE__ */ new Set(),
|
|
45
|
+
selectAllPages: false,
|
|
46
|
+
activeFilters: {},
|
|
47
|
+
sortBy,
|
|
48
|
+
sortDirection,
|
|
49
|
+
searchTerm,
|
|
50
|
+
debouncedSearchTerm,
|
|
51
|
+
loading,
|
|
52
|
+
loadingRows,
|
|
53
|
+
error,
|
|
54
|
+
viewMode
|
|
55
|
+
}), [
|
|
56
|
+
data,
|
|
57
|
+
currentPage,
|
|
58
|
+
pageSize,
|
|
59
|
+
totalPages,
|
|
60
|
+
totalCount,
|
|
61
|
+
sortBy,
|
|
62
|
+
sortDirection,
|
|
63
|
+
searchTerm,
|
|
64
|
+
debouncedSearchTerm,
|
|
65
|
+
loading,
|
|
66
|
+
loadingRows,
|
|
67
|
+
error,
|
|
68
|
+
viewMode
|
|
69
|
+
]);
|
|
70
|
+
const setData = useCallback((newData) => {
|
|
71
|
+
setDataState(newData);
|
|
72
|
+
}, []);
|
|
73
|
+
const setPage = useCallback((page) => {
|
|
74
|
+
setCurrentPage(page);
|
|
75
|
+
}, []);
|
|
76
|
+
const setPageSize = useCallback((size) => {
|
|
77
|
+
setPageSizeState(size);
|
|
78
|
+
setCurrentPage(1);
|
|
79
|
+
}, []);
|
|
80
|
+
const setLoading = useCallback((isLoading) => {
|
|
81
|
+
setLoadingState(isLoading);
|
|
82
|
+
}, []);
|
|
83
|
+
const setLoadingRows = useCallback((ids) => {
|
|
84
|
+
setLoadingRowsState(ids);
|
|
85
|
+
}, []);
|
|
86
|
+
const setError = useCallback((err) => {
|
|
87
|
+
setErrorState(err);
|
|
88
|
+
}, []);
|
|
89
|
+
const setViewMode = useCallback((mode) => {
|
|
90
|
+
setViewModeState(mode);
|
|
91
|
+
}, []);
|
|
92
|
+
return {
|
|
93
|
+
state,
|
|
94
|
+
setData,
|
|
95
|
+
setPage,
|
|
96
|
+
setPageSize,
|
|
97
|
+
setLoading,
|
|
98
|
+
setLoadingRows,
|
|
99
|
+
setError,
|
|
100
|
+
setViewMode
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function useSelection({
|
|
104
|
+
currentPageData,
|
|
105
|
+
totalCount,
|
|
106
|
+
getRowId,
|
|
107
|
+
onSelectionChange,
|
|
108
|
+
onSelectAllPages,
|
|
109
|
+
externalSelectedIds,
|
|
110
|
+
externalSelectAllPages
|
|
111
|
+
}) {
|
|
112
|
+
const [internalSelectedIds, setInternalSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
113
|
+
const [internalSelectAllPages, setInternalSelectAllPages] = useState(false);
|
|
114
|
+
const isControlled = externalSelectedIds !== void 0;
|
|
115
|
+
const selectedIds = isControlled ? externalSelectedIds : internalSelectedIds;
|
|
116
|
+
const selectAllPages = externalSelectAllPages ?? internalSelectAllPages;
|
|
117
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
118
|
+
const onSelectAllPagesRef = useRef(onSelectAllPages);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
121
|
+
onSelectAllPagesRef.current = onSelectAllPages;
|
|
122
|
+
}, [onSelectionChange, onSelectAllPages]);
|
|
123
|
+
const prevExternalIdsRef = useRef(void 0);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (externalSelectedIds !== void 0 && externalSelectedIds !== prevExternalIdsRef.current) {
|
|
126
|
+
setInternalSelectedIds(new Set(externalSelectedIds));
|
|
127
|
+
prevExternalIdsRef.current = externalSelectedIds;
|
|
128
|
+
}
|
|
129
|
+
}, [externalSelectedIds]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (externalSelectAllPages !== void 0) {
|
|
132
|
+
setInternalSelectAllPages(externalSelectAllPages);
|
|
133
|
+
}
|
|
134
|
+
}, [externalSelectAllPages]);
|
|
135
|
+
const toggleRow = useCallback((id) => {
|
|
136
|
+
setInternalSelectedIds((prev) => {
|
|
137
|
+
const next = new Set(prev);
|
|
138
|
+
if (next.has(id)) {
|
|
139
|
+
next.delete(id);
|
|
140
|
+
} else {
|
|
141
|
+
next.add(id);
|
|
142
|
+
}
|
|
143
|
+
onSelectionChangeRef.current?.(next);
|
|
144
|
+
return next;
|
|
145
|
+
});
|
|
146
|
+
setInternalSelectAllPages((prev) => {
|
|
147
|
+
if (prev) {
|
|
148
|
+
onSelectAllPagesRef.current?.(false);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return prev;
|
|
152
|
+
});
|
|
153
|
+
}, []);
|
|
154
|
+
const toggleAll = useCallback(() => {
|
|
155
|
+
const currentPageIds = currentPageData.map(getRowId);
|
|
156
|
+
setInternalSelectedIds((prev) => {
|
|
157
|
+
const allSelected = currentPageIds.every((id) => prev.has(id));
|
|
158
|
+
let next;
|
|
159
|
+
if (allSelected) {
|
|
160
|
+
next = new Set(prev);
|
|
161
|
+
currentPageIds.forEach((id) => next.delete(id));
|
|
162
|
+
} else {
|
|
163
|
+
next = new Set(prev);
|
|
164
|
+
currentPageIds.forEach((id) => next.add(id));
|
|
165
|
+
}
|
|
166
|
+
onSelectionChangeRef.current?.(next);
|
|
167
|
+
return next;
|
|
168
|
+
});
|
|
169
|
+
setInternalSelectAllPages((prev) => {
|
|
170
|
+
if (prev) {
|
|
171
|
+
onSelectAllPagesRef.current?.(false);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return prev;
|
|
175
|
+
});
|
|
176
|
+
}, [currentPageData, getRowId]);
|
|
177
|
+
const selectAllPagesToggle = useCallback(() => {
|
|
178
|
+
setInternalSelectAllPages((prev) => {
|
|
179
|
+
const next = !prev;
|
|
180
|
+
onSelectAllPagesRef.current?.(next);
|
|
181
|
+
if (next) {
|
|
182
|
+
setInternalSelectedIds(/* @__PURE__ */ new Set());
|
|
183
|
+
onSelectionChangeRef.current?.(/* @__PURE__ */ new Set());
|
|
184
|
+
}
|
|
185
|
+
return next;
|
|
186
|
+
});
|
|
187
|
+
}, []);
|
|
188
|
+
const clearSelection = useCallback(() => {
|
|
189
|
+
setInternalSelectedIds(/* @__PURE__ */ new Set());
|
|
190
|
+
setInternalSelectAllPages(false);
|
|
191
|
+
onSelectionChangeRef.current?.(/* @__PURE__ */ new Set());
|
|
192
|
+
onSelectAllPagesRef.current?.(false);
|
|
193
|
+
}, []);
|
|
194
|
+
const getSelectedCount = useCallback(() => {
|
|
195
|
+
if (selectAllPages) {
|
|
196
|
+
return totalCount;
|
|
197
|
+
}
|
|
198
|
+
return selectedIds.size;
|
|
199
|
+
}, [selectAllPages, selectedIds, totalCount]);
|
|
200
|
+
return {
|
|
201
|
+
selectedIds,
|
|
202
|
+
selectAllPages,
|
|
203
|
+
toggleRow,
|
|
204
|
+
toggleAll,
|
|
205
|
+
selectAllPagesToggle,
|
|
206
|
+
clearSelection,
|
|
207
|
+
getSelectedCount
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function usePagination({
|
|
211
|
+
totalCount,
|
|
212
|
+
initialPageSize = 25,
|
|
213
|
+
initialPage = 1,
|
|
214
|
+
serverSide = false,
|
|
215
|
+
onPageChange
|
|
216
|
+
}) {
|
|
217
|
+
const [currentPage, setCurrentPage] = useState(initialPage);
|
|
218
|
+
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
219
|
+
const totalPages = useMemo(() => {
|
|
220
|
+
return Math.max(1, Math.ceil(totalCount / pageSize));
|
|
221
|
+
}, [totalCount, pageSize]);
|
|
222
|
+
const canGoNext = useMemo(() => {
|
|
223
|
+
return currentPage < totalPages;
|
|
224
|
+
}, [currentPage, totalPages]);
|
|
225
|
+
const canGoPrevious = useMemo(() => {
|
|
226
|
+
return currentPage > 1;
|
|
227
|
+
}, [currentPage]);
|
|
228
|
+
const goToPage = useCallback((page) => {
|
|
229
|
+
const validPage = Math.max(1, Math.min(page, totalPages));
|
|
230
|
+
setCurrentPage(validPage);
|
|
231
|
+
onPageChange?.(validPage);
|
|
232
|
+
}, [totalPages, onPageChange]);
|
|
233
|
+
const goToFirstPage = useCallback(() => {
|
|
234
|
+
goToPage(1);
|
|
235
|
+
}, [goToPage]);
|
|
236
|
+
const goToLastPage = useCallback(() => {
|
|
237
|
+
goToPage(totalPages);
|
|
238
|
+
}, [goToPage, totalPages]);
|
|
239
|
+
const goToNextPage = useCallback(() => {
|
|
240
|
+
if (canGoNext) {
|
|
241
|
+
goToPage(currentPage + 1);
|
|
242
|
+
}
|
|
243
|
+
}, [canGoNext, currentPage, goToPage]);
|
|
244
|
+
const goToPreviousPage = useCallback(() => {
|
|
245
|
+
if (canGoPrevious) {
|
|
246
|
+
goToPage(currentPage - 1);
|
|
247
|
+
}
|
|
248
|
+
}, [canGoPrevious, currentPage, goToPage]);
|
|
249
|
+
const getPageNumbers = useCallback(() => {
|
|
250
|
+
const maxVisible = 7;
|
|
251
|
+
if (totalPages <= maxVisible) {
|
|
252
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
253
|
+
}
|
|
254
|
+
const pages = [];
|
|
255
|
+
pages.push(1);
|
|
256
|
+
if (currentPage > 3) {
|
|
257
|
+
pages.push("...");
|
|
258
|
+
}
|
|
259
|
+
const start = Math.max(2, currentPage - 1);
|
|
260
|
+
const end = Math.min(totalPages - 1, currentPage + 1);
|
|
261
|
+
for (let i = start; i <= end; i++) {
|
|
262
|
+
if (!pages.includes(i)) {
|
|
263
|
+
pages.push(i);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (currentPage < totalPages - 2) {
|
|
267
|
+
pages.push("...");
|
|
268
|
+
}
|
|
269
|
+
if (!pages.includes(totalPages)) {
|
|
270
|
+
pages.push(totalPages);
|
|
271
|
+
}
|
|
272
|
+
return pages;
|
|
273
|
+
}, [currentPage, totalPages]);
|
|
274
|
+
const getDisplayRange = useCallback(() => {
|
|
275
|
+
const start = (currentPage - 1) * pageSize + 1;
|
|
276
|
+
const end = Math.min(currentPage * pageSize, totalCount);
|
|
277
|
+
return { start, end };
|
|
278
|
+
}, [currentPage, pageSize, totalCount]);
|
|
279
|
+
return {
|
|
280
|
+
currentPage,
|
|
281
|
+
pageSize,
|
|
282
|
+
totalPages,
|
|
283
|
+
totalCount,
|
|
284
|
+
canGoNext,
|
|
285
|
+
canGoPrevious,
|
|
286
|
+
goToPage,
|
|
287
|
+
goToFirstPage,
|
|
288
|
+
goToLastPage,
|
|
289
|
+
goToNextPage,
|
|
290
|
+
goToPreviousPage,
|
|
291
|
+
getPageNumbers,
|
|
292
|
+
getDisplayRange
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function useFilters({
|
|
296
|
+
initialFilters = {},
|
|
297
|
+
onChange
|
|
298
|
+
}) {
|
|
299
|
+
const [filters, setFilters] = useState(initialFilters);
|
|
300
|
+
const setFilter = useCallback((key, value) => {
|
|
301
|
+
setFilters((prev) => {
|
|
302
|
+
const next = { ...prev, [key]: value };
|
|
303
|
+
onChange?.(next);
|
|
304
|
+
return next;
|
|
305
|
+
});
|
|
306
|
+
}, [onChange]);
|
|
307
|
+
const clearFilter = useCallback((key) => {
|
|
308
|
+
setFilters((prev) => {
|
|
309
|
+
const next = { ...prev };
|
|
310
|
+
delete next[key];
|
|
311
|
+
onChange?.(next);
|
|
312
|
+
return next;
|
|
313
|
+
});
|
|
314
|
+
}, [onChange]);
|
|
315
|
+
const clearAllFilters = useCallback(() => {
|
|
316
|
+
setFilters({});
|
|
317
|
+
onChange?.({});
|
|
318
|
+
}, [onChange]);
|
|
319
|
+
const hasActiveFilters = useCallback(() => {
|
|
320
|
+
return Object.keys(filters).length > 0;
|
|
321
|
+
}, [filters]);
|
|
322
|
+
const getActiveFilterCount = useCallback(() => {
|
|
323
|
+
return Object.keys(filters).length;
|
|
324
|
+
}, [filters]);
|
|
325
|
+
return {
|
|
326
|
+
filters,
|
|
327
|
+
setFilter,
|
|
328
|
+
clearFilter,
|
|
329
|
+
clearAllFilters,
|
|
330
|
+
hasActiveFilters,
|
|
331
|
+
getActiveFilterCount
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
var DEFAULT_BREAKPOINTS = {
|
|
335
|
+
mobile: 768,
|
|
336
|
+
tablet: 1024,
|
|
337
|
+
desktop: 1280
|
|
338
|
+
};
|
|
339
|
+
function useResponsive(breakpoints = DEFAULT_BREAKPOINTS) {
|
|
340
|
+
const [viewMode, setViewMode] = useState("desktop");
|
|
341
|
+
const [windowWidth, setWindowWidth] = useState(0);
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
const handleResize = () => {
|
|
344
|
+
const width = window.innerWidth;
|
|
345
|
+
setWindowWidth(width);
|
|
346
|
+
if (width < breakpoints.mobile) {
|
|
347
|
+
setViewMode("mobile");
|
|
348
|
+
} else if (width < breakpoints.tablet) {
|
|
349
|
+
setViewMode("tablet");
|
|
350
|
+
} else {
|
|
351
|
+
setViewMode("desktop");
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
handleResize();
|
|
355
|
+
window.addEventListener("resize", handleResize);
|
|
356
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
357
|
+
}, [breakpoints]);
|
|
358
|
+
return {
|
|
359
|
+
viewMode,
|
|
360
|
+
windowWidth,
|
|
361
|
+
isMobile: viewMode === "mobile",
|
|
362
|
+
isTablet: viewMode === "tablet",
|
|
363
|
+
isDesktop: viewMode === "desktop",
|
|
364
|
+
isMobileOrTablet: viewMode === "mobile" || viewMode === "tablet"
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function useColumnVisibility({
|
|
368
|
+
defaultVisibility = {},
|
|
369
|
+
persistKey,
|
|
370
|
+
onVisibilityChange
|
|
371
|
+
} = {}) {
|
|
372
|
+
const [columnVisibility, setColumnVisibility] = useState(() => {
|
|
373
|
+
if (persistKey && typeof window !== "undefined") {
|
|
374
|
+
const stored = localStorage.getItem(`column-visibility-${persistKey}`);
|
|
375
|
+
if (stored) {
|
|
376
|
+
try {
|
|
377
|
+
return JSON.parse(stored);
|
|
378
|
+
} catch {
|
|
379
|
+
return defaultVisibility;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return defaultVisibility;
|
|
384
|
+
});
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
if (persistKey && typeof window !== "undefined") {
|
|
387
|
+
localStorage.setItem(`column-visibility-${persistKey}`, JSON.stringify(columnVisibility));
|
|
388
|
+
}
|
|
389
|
+
onVisibilityChange?.(columnVisibility);
|
|
390
|
+
}, [columnVisibility, persistKey, onVisibilityChange]);
|
|
391
|
+
const toggleColumn = useCallback((columnId) => {
|
|
392
|
+
setColumnVisibility((prev) => ({
|
|
393
|
+
...prev,
|
|
394
|
+
[columnId]: !(prev[columnId] ?? true)
|
|
395
|
+
}));
|
|
396
|
+
}, []);
|
|
397
|
+
const showColumn = useCallback((columnId) => {
|
|
398
|
+
setColumnVisibility((prev) => ({
|
|
399
|
+
...prev,
|
|
400
|
+
[columnId]: true
|
|
401
|
+
}));
|
|
402
|
+
}, []);
|
|
403
|
+
const hideColumn = useCallback((columnId) => {
|
|
404
|
+
setColumnVisibility((prev) => ({
|
|
405
|
+
...prev,
|
|
406
|
+
[columnId]: false
|
|
407
|
+
}));
|
|
408
|
+
}, []);
|
|
409
|
+
const showAllColumns = useCallback(() => {
|
|
410
|
+
setColumnVisibility({});
|
|
411
|
+
}, []);
|
|
412
|
+
const hideAllColumns = useCallback((exceptColumns = []) => {
|
|
413
|
+
setColumnVisibility((prev) => {
|
|
414
|
+
const newVisibility = {};
|
|
415
|
+
Object.keys(prev).forEach((key) => {
|
|
416
|
+
newVisibility[key] = exceptColumns.includes(key);
|
|
417
|
+
});
|
|
418
|
+
return newVisibility;
|
|
419
|
+
});
|
|
420
|
+
}, []);
|
|
421
|
+
const resetVisibility = useCallback(() => {
|
|
422
|
+
setColumnVisibility(defaultVisibility);
|
|
423
|
+
}, [defaultVisibility]);
|
|
424
|
+
const isColumnVisible = useCallback((columnId) => {
|
|
425
|
+
return columnVisibility[columnId] ?? true;
|
|
426
|
+
}, [columnVisibility]);
|
|
427
|
+
return {
|
|
428
|
+
columnVisibility,
|
|
429
|
+
setColumnVisibility,
|
|
430
|
+
toggleColumn,
|
|
431
|
+
showColumn,
|
|
432
|
+
hideColumn,
|
|
433
|
+
showAllColumns,
|
|
434
|
+
hideAllColumns,
|
|
435
|
+
resetVisibility,
|
|
436
|
+
isColumnVisible
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function useTablePreferences({
|
|
440
|
+
tableId,
|
|
441
|
+
enabled = true,
|
|
442
|
+
defaultColumnVisibility = {},
|
|
443
|
+
defaultPageSize = 25,
|
|
444
|
+
debounceMs = 500,
|
|
445
|
+
apiFetch = fetch
|
|
446
|
+
// Default to native fetch if not provided
|
|
447
|
+
}) {
|
|
448
|
+
const [preferences, setPreferences] = useState(null);
|
|
449
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
450
|
+
const [error, setError] = useState(null);
|
|
451
|
+
const saveTimeoutRef = useRef(null);
|
|
452
|
+
const pendingUpdatesRef = useRef({});
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (!enabled) {
|
|
455
|
+
setIsLoading(false);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const loadPreferences = async () => {
|
|
459
|
+
try {
|
|
460
|
+
const response = await apiFetch(`/api/v1/table-preferences/?tableId=${encodeURIComponent(tableId)}`);
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
throw new Error("Failed to load table preferences");
|
|
463
|
+
}
|
|
464
|
+
const data = await response.json();
|
|
465
|
+
if (data) {
|
|
466
|
+
setPreferences(data);
|
|
467
|
+
} else {
|
|
468
|
+
setPreferences({
|
|
469
|
+
columnVisibility: defaultColumnVisibility,
|
|
470
|
+
columnOrder: null,
|
|
471
|
+
defaultSortColumn: null,
|
|
472
|
+
defaultSortDirection: null,
|
|
473
|
+
pageSize: defaultPageSize
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
} catch (err) {
|
|
477
|
+
console.error("Error loading table preferences:", err);
|
|
478
|
+
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
479
|
+
setPreferences({
|
|
480
|
+
columnVisibility: defaultColumnVisibility,
|
|
481
|
+
columnOrder: null,
|
|
482
|
+
defaultSortColumn: null,
|
|
483
|
+
defaultSortDirection: null,
|
|
484
|
+
pageSize: defaultPageSize
|
|
485
|
+
});
|
|
486
|
+
} finally {
|
|
487
|
+
setIsLoading(false);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
loadPreferences();
|
|
491
|
+
}, [tableId, enabled, defaultPageSize]);
|
|
492
|
+
const savePreferences = useCallback(async (updates) => {
|
|
493
|
+
pendingUpdatesRef.current = { ...pendingUpdatesRef.current, ...updates };
|
|
494
|
+
if (saveTimeoutRef.current) {
|
|
495
|
+
clearTimeout(saveTimeoutRef.current);
|
|
496
|
+
}
|
|
497
|
+
saveTimeoutRef.current = setTimeout(async () => {
|
|
498
|
+
const updatesToSave = { ...pendingUpdatesRef.current };
|
|
499
|
+
pendingUpdatesRef.current = {};
|
|
500
|
+
try {
|
|
501
|
+
const response = await apiFetch("/api/v1/table-preferences/", {
|
|
502
|
+
method: "POST",
|
|
503
|
+
headers: { "Content-Type": "application/json" },
|
|
504
|
+
body: JSON.stringify({
|
|
505
|
+
tableId,
|
|
506
|
+
...updatesToSave
|
|
507
|
+
})
|
|
508
|
+
});
|
|
509
|
+
if (!response.ok) {
|
|
510
|
+
throw new Error("Failed to save table preferences");
|
|
511
|
+
}
|
|
512
|
+
} catch (err) {
|
|
513
|
+
console.error("Error saving table preferences:", err);
|
|
514
|
+
}
|
|
515
|
+
}, debounceMs);
|
|
516
|
+
}, [tableId, debounceMs]);
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
return () => {
|
|
519
|
+
if (saveTimeoutRef.current) {
|
|
520
|
+
clearTimeout(saveTimeoutRef.current);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}, []);
|
|
524
|
+
const updateColumnVisibility = useCallback((visibility) => {
|
|
525
|
+
setPreferences((prev) => prev ? { ...prev, columnVisibility: visibility } : null);
|
|
526
|
+
savePreferences({ columnVisibility: visibility });
|
|
527
|
+
}, [savePreferences]);
|
|
528
|
+
const updateColumnOrder = useCallback((order) => {
|
|
529
|
+
setPreferences((prev) => prev ? { ...prev, columnOrder: order } : null);
|
|
530
|
+
savePreferences({ columnOrder: order });
|
|
531
|
+
}, [savePreferences]);
|
|
532
|
+
const updateSortPreference = useCallback((sort) => {
|
|
533
|
+
setPreferences((prev) => prev ? {
|
|
534
|
+
...prev,
|
|
535
|
+
defaultSortColumn: sort.sortBy,
|
|
536
|
+
defaultSortDirection: sort.sortDirection
|
|
537
|
+
} : null);
|
|
538
|
+
savePreferences({
|
|
539
|
+
defaultSortColumn: sort.sortBy,
|
|
540
|
+
defaultSortDirection: sort.sortDirection
|
|
541
|
+
});
|
|
542
|
+
}, [savePreferences]);
|
|
543
|
+
const updatePageSize = useCallback((size) => {
|
|
544
|
+
setPreferences((prev) => prev ? { ...prev, pageSize: size } : null);
|
|
545
|
+
savePreferences({ pageSize: size });
|
|
546
|
+
}, [savePreferences]);
|
|
547
|
+
const resetPreferences = useCallback(async () => {
|
|
548
|
+
try {
|
|
549
|
+
const response = await apiFetch(`/api/v1/table-preferences/?tableId=${encodeURIComponent(tableId)}`, {
|
|
550
|
+
method: "DELETE"
|
|
551
|
+
});
|
|
552
|
+
if (!response.ok) {
|
|
553
|
+
throw new Error("Failed to reset table preferences");
|
|
554
|
+
}
|
|
555
|
+
setPreferences({
|
|
556
|
+
columnVisibility: defaultColumnVisibility,
|
|
557
|
+
columnOrder: null,
|
|
558
|
+
defaultSortColumn: null,
|
|
559
|
+
defaultSortDirection: null,
|
|
560
|
+
pageSize: defaultPageSize
|
|
561
|
+
});
|
|
562
|
+
} catch (err) {
|
|
563
|
+
console.error("Error resetting table preferences:", err);
|
|
564
|
+
throw err;
|
|
565
|
+
}
|
|
566
|
+
}, [tableId, defaultColumnVisibility, defaultPageSize]);
|
|
567
|
+
return {
|
|
568
|
+
preferences,
|
|
569
|
+
isLoading,
|
|
570
|
+
error,
|
|
571
|
+
updateColumnVisibility,
|
|
572
|
+
updateColumnOrder,
|
|
573
|
+
updateSortPreference,
|
|
574
|
+
updatePageSize,
|
|
575
|
+
resetPreferences
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function useTableKeyboard({
|
|
579
|
+
data,
|
|
580
|
+
getRowId,
|
|
581
|
+
selectedIds,
|
|
582
|
+
onToggleRow,
|
|
583
|
+
onToggleAll,
|
|
584
|
+
onRowClick,
|
|
585
|
+
onDelete,
|
|
586
|
+
tableRef,
|
|
587
|
+
enabled = true
|
|
588
|
+
}) {
|
|
589
|
+
const [focusedRowIndex, setFocusedRowIndexState] = useState(-1);
|
|
590
|
+
const focusedRowIndexRef = useRef(-1);
|
|
591
|
+
const setFocusedRowIndex = useCallback((index) => {
|
|
592
|
+
focusedRowIndexRef.current = index;
|
|
593
|
+
setFocusedRowIndexState(index);
|
|
594
|
+
}, []);
|
|
595
|
+
const handleKeyDown = useCallback((event) => {
|
|
596
|
+
if (!enabled || data.length === 0) return;
|
|
597
|
+
const { key, ctrlKey, metaKey, shiftKey } = event;
|
|
598
|
+
const isModifierPressed = ctrlKey || metaKey;
|
|
599
|
+
if (key === "a" && isModifierPressed && onToggleAll) {
|
|
600
|
+
event.preventDefault();
|
|
601
|
+
onToggleAll();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (key === "Delete" && onDelete && selectedIds && selectedIds.size > 0) {
|
|
605
|
+
event.preventDefault();
|
|
606
|
+
onDelete(selectedIds);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (key === "ArrowDown") {
|
|
610
|
+
event.preventDefault();
|
|
611
|
+
const nextIndex = Math.min(focusedRowIndexRef.current + 1, data.length - 1);
|
|
612
|
+
setFocusedRowIndex(nextIndex);
|
|
613
|
+
if (tableRef.current) {
|
|
614
|
+
const rowElement = tableRef.current.querySelector(
|
|
615
|
+
`[data-row-index="${nextIndex}"]`
|
|
616
|
+
);
|
|
617
|
+
if (rowElement && typeof rowElement.focus === "function") {
|
|
618
|
+
rowElement.focus();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (key === "ArrowUp") {
|
|
624
|
+
event.preventDefault();
|
|
625
|
+
const prevIndex = Math.max(focusedRowIndexRef.current - 1, 0);
|
|
626
|
+
setFocusedRowIndex(prevIndex);
|
|
627
|
+
if (tableRef.current) {
|
|
628
|
+
const rowElement = tableRef.current.querySelector(
|
|
629
|
+
`[data-row-index="${prevIndex}"]`
|
|
630
|
+
);
|
|
631
|
+
if (rowElement) {
|
|
632
|
+
rowElement.focus();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (key === "Enter" && focusedRowIndexRef.current >= 0 && onRowClick) {
|
|
638
|
+
event.preventDefault();
|
|
639
|
+
const row = data[focusedRowIndexRef.current];
|
|
640
|
+
if (row) {
|
|
641
|
+
onRowClick(row);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (key === " " && focusedRowIndexRef.current >= 0 && onToggleRow) {
|
|
646
|
+
event.preventDefault();
|
|
647
|
+
const row = data[focusedRowIndexRef.current];
|
|
648
|
+
if (row) {
|
|
649
|
+
const rowId = getRowId(row);
|
|
650
|
+
onToggleRow(rowId);
|
|
651
|
+
}
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (key === "Home") {
|
|
655
|
+
event.preventDefault();
|
|
656
|
+
setFocusedRowIndex(0);
|
|
657
|
+
if (tableRef.current) {
|
|
658
|
+
const rowElement = tableRef.current.querySelector(
|
|
659
|
+
'[data-row-index="0"]'
|
|
660
|
+
);
|
|
661
|
+
if (rowElement) {
|
|
662
|
+
rowElement.focus();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (key === "End") {
|
|
668
|
+
event.preventDefault();
|
|
669
|
+
const lastIndex = data.length - 1;
|
|
670
|
+
setFocusedRowIndex(lastIndex);
|
|
671
|
+
if (tableRef.current) {
|
|
672
|
+
const rowElement = tableRef.current.querySelector(
|
|
673
|
+
`[data-row-index="${lastIndex}"]`
|
|
674
|
+
);
|
|
675
|
+
if (rowElement) {
|
|
676
|
+
rowElement.focus();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
}, [
|
|
682
|
+
enabled,
|
|
683
|
+
data,
|
|
684
|
+
getRowId,
|
|
685
|
+
selectedIds,
|
|
686
|
+
onToggleRow,
|
|
687
|
+
onToggleAll,
|
|
688
|
+
onRowClick,
|
|
689
|
+
onDelete,
|
|
690
|
+
tableRef,
|
|
691
|
+
setFocusedRowIndex
|
|
692
|
+
]);
|
|
693
|
+
useEffect(() => {
|
|
694
|
+
if (!enabled || !tableRef.current) return;
|
|
695
|
+
const handleKeyboardEvent = (event) => {
|
|
696
|
+
const syntheticEvent = {
|
|
697
|
+
key: event.key,
|
|
698
|
+
ctrlKey: event.ctrlKey,
|
|
699
|
+
metaKey: event.metaKey,
|
|
700
|
+
shiftKey: event.shiftKey,
|
|
701
|
+
preventDefault: () => event.preventDefault(),
|
|
702
|
+
stopPropagation: () => event.stopPropagation()
|
|
703
|
+
};
|
|
704
|
+
handleKeyDown(syntheticEvent);
|
|
705
|
+
};
|
|
706
|
+
const tableElement = tableRef.current;
|
|
707
|
+
tableElement.addEventListener("keydown", handleKeyboardEvent);
|
|
708
|
+
return () => {
|
|
709
|
+
tableElement.removeEventListener("keydown", handleKeyboardEvent);
|
|
710
|
+
};
|
|
711
|
+
}, [enabled, tableRef, handleKeyDown]);
|
|
712
|
+
return {
|
|
713
|
+
focusedRowIndex,
|
|
714
|
+
setFocusedRowIndex,
|
|
715
|
+
handleKeyDown
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function useTableURL({
|
|
719
|
+
tableId,
|
|
720
|
+
persistToUrl,
|
|
721
|
+
debounceMs = 300
|
|
722
|
+
}) {
|
|
723
|
+
const searchParams = useSearchParams();
|
|
724
|
+
const router = useRouter();
|
|
725
|
+
const pathname = usePathname();
|
|
726
|
+
const debounceTimerRef = useRef(null);
|
|
727
|
+
const isApplyingURLRef = useRef(false);
|
|
728
|
+
const getParamKey = useCallback((key) => {
|
|
729
|
+
return `${tableId}_${key}`;
|
|
730
|
+
}, [tableId]);
|
|
731
|
+
const getURLState = useCallback(() => {
|
|
732
|
+
if (!persistToUrl) {
|
|
733
|
+
return {
|
|
734
|
+
sortBy: null,
|
|
735
|
+
sortDirection: "asc",
|
|
736
|
+
filters: {},
|
|
737
|
+
page: 1,
|
|
738
|
+
search: ""
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
const sortBy = searchParams.get(getParamKey("sortBy"));
|
|
742
|
+
const sortDirection = searchParams.get(getParamKey("sortDir"));
|
|
743
|
+
const page = parseInt(searchParams.get(getParamKey("page")) || "1", 10);
|
|
744
|
+
const search = searchParams.get(getParamKey("search")) || "";
|
|
745
|
+
const filters = {};
|
|
746
|
+
const filterPrefix = getParamKey("filter_");
|
|
747
|
+
searchParams.forEach((value, key) => {
|
|
748
|
+
if (key.startsWith(filterPrefix)) {
|
|
749
|
+
const filterKey = key.substring(filterPrefix.length);
|
|
750
|
+
try {
|
|
751
|
+
filters[filterKey] = JSON.parse(value);
|
|
752
|
+
} catch {
|
|
753
|
+
filters[filterKey] = value;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
return {
|
|
758
|
+
sortBy: sortBy || null,
|
|
759
|
+
sortDirection: sortDirection || "asc",
|
|
760
|
+
filters,
|
|
761
|
+
page: isNaN(page) || page < 1 ? 1 : page,
|
|
762
|
+
search
|
|
763
|
+
};
|
|
764
|
+
}, [searchParams, persistToUrl, getParamKey]);
|
|
765
|
+
const updateURL = useCallback((updates) => {
|
|
766
|
+
if (!persistToUrl) return;
|
|
767
|
+
if (debounceTimerRef.current) {
|
|
768
|
+
clearTimeout(debounceTimerRef.current);
|
|
769
|
+
}
|
|
770
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
771
|
+
const newParams = new URLSearchParams(searchParams.toString());
|
|
772
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
773
|
+
if (value === null || value === "") {
|
|
774
|
+
newParams.delete(key);
|
|
775
|
+
} else {
|
|
776
|
+
newParams.set(key, value);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
const newParamsString = newParams.toString();
|
|
780
|
+
const currentParamsString = searchParams.toString();
|
|
781
|
+
if (newParamsString !== currentParamsString) {
|
|
782
|
+
isApplyingURLRef.current = true;
|
|
783
|
+
const newURL = newParamsString ? `${pathname}?${newParamsString}` : pathname;
|
|
784
|
+
router.replace(newURL, { scroll: false });
|
|
785
|
+
setTimeout(() => {
|
|
786
|
+
isApplyingURLRef.current = false;
|
|
787
|
+
}, 50);
|
|
788
|
+
}
|
|
789
|
+
}, debounceMs);
|
|
790
|
+
}, [persistToUrl, searchParams, pathname, router, debounceMs]);
|
|
791
|
+
const setSortToURL = useCallback((sort) => {
|
|
792
|
+
if (!persistToUrl) return;
|
|
793
|
+
updateURL({
|
|
794
|
+
[getParamKey("sortBy")]: sort.sortBy,
|
|
795
|
+
[getParamKey("sortDir")]: sort.sortBy ? sort.sortDirection : null,
|
|
796
|
+
// Reset to page 1 when sorting changes
|
|
797
|
+
[getParamKey("page")]: "1"
|
|
798
|
+
});
|
|
799
|
+
}, [persistToUrl, updateURL, getParamKey]);
|
|
800
|
+
const setFiltersToURL = useCallback((filters) => {
|
|
801
|
+
if (!persistToUrl) return;
|
|
802
|
+
const updates = {};
|
|
803
|
+
const filterPrefix = getParamKey("filter_");
|
|
804
|
+
searchParams.forEach((_, key) => {
|
|
805
|
+
if (key.startsWith(filterPrefix)) {
|
|
806
|
+
updates[key] = null;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
810
|
+
const paramKey = `${filterPrefix}${key}`;
|
|
811
|
+
if (value === void 0 || value === null || value === "") {
|
|
812
|
+
updates[paramKey] = null;
|
|
813
|
+
} else if (typeof value === "object") {
|
|
814
|
+
updates[paramKey] = JSON.stringify(value);
|
|
815
|
+
} else {
|
|
816
|
+
updates[paramKey] = String(value);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
updates[getParamKey("page")] = "1";
|
|
820
|
+
updateURL(updates);
|
|
821
|
+
}, [persistToUrl, updateURL, getParamKey, searchParams]);
|
|
822
|
+
const setPageToURL = useCallback((page) => {
|
|
823
|
+
if (!persistToUrl) return;
|
|
824
|
+
updateURL({
|
|
825
|
+
[getParamKey("page")]: page > 1 ? String(page) : null
|
|
826
|
+
// Remove param if page 1
|
|
827
|
+
});
|
|
828
|
+
}, [persistToUrl, updateURL, getParamKey]);
|
|
829
|
+
const setSearchToURL = useCallback((search) => {
|
|
830
|
+
if (!persistToUrl) return;
|
|
831
|
+
updateURL({
|
|
832
|
+
[getParamKey("search")]: search || null,
|
|
833
|
+
// Reset to page 1 when search changes
|
|
834
|
+
[getParamKey("page")]: "1"
|
|
835
|
+
});
|
|
836
|
+
}, [persistToUrl, updateURL, getParamKey]);
|
|
837
|
+
const clearURLState = useCallback(() => {
|
|
838
|
+
if (!persistToUrl) return;
|
|
839
|
+
const newParams = new URLSearchParams(searchParams.toString());
|
|
840
|
+
const keysToDelete = [];
|
|
841
|
+
newParams.forEach((_, key) => {
|
|
842
|
+
if (key.startsWith(`${tableId}_`)) {
|
|
843
|
+
keysToDelete.push(key);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
keysToDelete.forEach((key) => newParams.delete(key));
|
|
847
|
+
const newURL = newParams.toString() ? `${pathname}?${newParams.toString()}` : pathname;
|
|
848
|
+
router.replace(newURL, { scroll: false });
|
|
849
|
+
}, [persistToUrl, searchParams, pathname, router, tableId]);
|
|
850
|
+
const hasURLState = useCallback(() => {
|
|
851
|
+
if (!persistToUrl) return false;
|
|
852
|
+
let hasState = false;
|
|
853
|
+
searchParams.forEach((_, key) => {
|
|
854
|
+
if (key.startsWith(`${tableId}_`)) {
|
|
855
|
+
hasState = true;
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
return hasState;
|
|
859
|
+
}, [persistToUrl, searchParams, tableId]);
|
|
860
|
+
useEffect(() => {
|
|
861
|
+
return () => {
|
|
862
|
+
if (debounceTimerRef.current) {
|
|
863
|
+
clearTimeout(debounceTimerRef.current);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}, []);
|
|
867
|
+
return {
|
|
868
|
+
getURLState,
|
|
869
|
+
setSortToURL,
|
|
870
|
+
setFiltersToURL,
|
|
871
|
+
setPageToURL,
|
|
872
|
+
setSearchToURL,
|
|
873
|
+
clearURLState,
|
|
874
|
+
hasURLState
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function useColumnReorder({
|
|
878
|
+
columns,
|
|
879
|
+
initialOrder = null,
|
|
880
|
+
enabled = true,
|
|
881
|
+
onOrderChange
|
|
882
|
+
}) {
|
|
883
|
+
const defaultOrder = useMemo(() => columns.map((c) => c.id), [columns]);
|
|
884
|
+
const [columnOrder, setColumnOrder] = useState(() => {
|
|
885
|
+
if (initialOrder && initialOrder.length > 0) {
|
|
886
|
+
const validOrder = initialOrder.filter((id) => columns.some((c) => c.id === id));
|
|
887
|
+
const newColumns = columns.filter((c) => !initialOrder.includes(c.id)).map((c) => c.id);
|
|
888
|
+
return [...validOrder, ...newColumns];
|
|
889
|
+
}
|
|
890
|
+
return defaultOrder;
|
|
891
|
+
});
|
|
892
|
+
const orderedColumns = useMemo(() => {
|
|
893
|
+
if (!enabled) return columns;
|
|
894
|
+
const columnMap = new Map(columns.map((c) => [c.id, c]));
|
|
895
|
+
const ordered = [];
|
|
896
|
+
for (const id of columnOrder) {
|
|
897
|
+
const column = columnMap.get(id);
|
|
898
|
+
if (column) {
|
|
899
|
+
ordered.push(column);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
for (const column of columns) {
|
|
903
|
+
if (!columnOrder.includes(column.id)) {
|
|
904
|
+
ordered.push(column);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return ordered;
|
|
908
|
+
}, [columns, columnOrder, enabled]);
|
|
909
|
+
const handleDragEnd = useCallback((result) => {
|
|
910
|
+
if (!result.destination) return;
|
|
911
|
+
if (result.source.index === result.destination.index) return;
|
|
912
|
+
const newOrder = Array.from(columnOrder);
|
|
913
|
+
const [removed] = newOrder.splice(result.source.index, 1);
|
|
914
|
+
newOrder.splice(result.destination.index, 0, removed);
|
|
915
|
+
setColumnOrder(newOrder);
|
|
916
|
+
onOrderChange?.(newOrder);
|
|
917
|
+
}, [columnOrder, onOrderChange]);
|
|
918
|
+
const resetOrder = useCallback(() => {
|
|
919
|
+
setColumnOrder(defaultOrder);
|
|
920
|
+
onOrderChange?.(defaultOrder);
|
|
921
|
+
}, [defaultOrder, onOrderChange]);
|
|
922
|
+
return {
|
|
923
|
+
orderedColumns,
|
|
924
|
+
columnOrder,
|
|
925
|
+
handleDragEnd,
|
|
926
|
+
resetOrder
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function useColumnResize({
|
|
930
|
+
enabled = true,
|
|
931
|
+
initialWidths = null,
|
|
932
|
+
minWidth = 50,
|
|
933
|
+
onWidthChange
|
|
934
|
+
} = {}) {
|
|
935
|
+
const [columnWidths, setColumnWidths] = useState(() => initialWidths ?? {});
|
|
936
|
+
const [resizingColumn, setResizingColumn] = useState(null);
|
|
937
|
+
const resizeRef = useRef(null);
|
|
938
|
+
const minWidthRef = useRef(minWidth);
|
|
939
|
+
const onWidthChangeRef = useRef(onWidthChange);
|
|
940
|
+
useEffect(() => {
|
|
941
|
+
minWidthRef.current = minWidth;
|
|
942
|
+
onWidthChangeRef.current = onWidthChange;
|
|
943
|
+
}, [minWidth, onWidthChange]);
|
|
944
|
+
const getColumnWidth = useCallback((columnId, defaultWidth) => {
|
|
945
|
+
if (!enabled) return defaultWidth;
|
|
946
|
+
return columnWidths[columnId] ?? defaultWidth;
|
|
947
|
+
}, [enabled, columnWidths]);
|
|
948
|
+
const startResize = useCallback((columnId, startX, startWidth) => {
|
|
949
|
+
if (!enabled) return;
|
|
950
|
+
resizeRef.current = { columnId, startX, startWidth };
|
|
951
|
+
setResizingColumn(columnId);
|
|
952
|
+
}, [enabled]);
|
|
953
|
+
const handlePointerMove = useCallback((e) => {
|
|
954
|
+
if (!resizeRef.current) return;
|
|
955
|
+
const { columnId, startX, startWidth } = resizeRef.current;
|
|
956
|
+
const diff = e.clientX - startX;
|
|
957
|
+
const newWidth = Math.max(minWidthRef.current, startWidth + diff);
|
|
958
|
+
setColumnWidths((prev) => ({ ...prev, [columnId]: newWidth }));
|
|
959
|
+
}, []);
|
|
960
|
+
const handlePointerUp = useCallback(() => {
|
|
961
|
+
if (!resizeRef.current) return;
|
|
962
|
+
resizeRef.current = null;
|
|
963
|
+
setResizingColumn(null);
|
|
964
|
+
setColumnWidths((current) => {
|
|
965
|
+
onWidthChangeRef.current?.(current);
|
|
966
|
+
return current;
|
|
967
|
+
});
|
|
968
|
+
}, []);
|
|
969
|
+
useEffect(() => {
|
|
970
|
+
if (resizingColumn) {
|
|
971
|
+
document.addEventListener("pointermove", handlePointerMove);
|
|
972
|
+
document.addEventListener("pointerup", handlePointerUp);
|
|
973
|
+
document.addEventListener("mousemove", handlePointerMove);
|
|
974
|
+
document.addEventListener("mouseup", handlePointerUp);
|
|
975
|
+
document.body.style.cursor = "col-resize";
|
|
976
|
+
document.body.style.userSelect = "none";
|
|
977
|
+
return () => {
|
|
978
|
+
document.removeEventListener("pointermove", handlePointerMove);
|
|
979
|
+
document.removeEventListener("pointerup", handlePointerUp);
|
|
980
|
+
document.removeEventListener("mousemove", handlePointerMove);
|
|
981
|
+
document.removeEventListener("mouseup", handlePointerUp);
|
|
982
|
+
document.body.style.cursor = "";
|
|
983
|
+
document.body.style.userSelect = "";
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
}, [resizingColumn, handlePointerMove, handlePointerUp]);
|
|
987
|
+
const resetWidths = useCallback(() => {
|
|
988
|
+
setColumnWidths({});
|
|
989
|
+
onWidthChange?.({});
|
|
990
|
+
}, [onWidthChange]);
|
|
991
|
+
return {
|
|
992
|
+
columnWidths,
|
|
993
|
+
getColumnWidth,
|
|
994
|
+
startResize,
|
|
995
|
+
resetWidths,
|
|
996
|
+
isResizing: resizingColumn !== null,
|
|
997
|
+
resizingColumn
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
var buttonVariants = cva(
|
|
1001
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
1002
|
+
{
|
|
1003
|
+
variants: {
|
|
1004
|
+
variant: {
|
|
1005
|
+
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
1006
|
+
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
1007
|
+
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
1008
|
+
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
1009
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
1010
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
1011
|
+
},
|
|
1012
|
+
size: {
|
|
1013
|
+
default: "h-9 px-4 py-2",
|
|
1014
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
1015
|
+
lg: "h-10 rounded-md px-8",
|
|
1016
|
+
icon: "h-9 w-9"
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
defaultVariants: {
|
|
1020
|
+
variant: "default",
|
|
1021
|
+
size: "default"
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
);
|
|
1025
|
+
var Button = React5.forwardRef(
|
|
1026
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
1027
|
+
const Comp = asChild ? Slot : "button";
|
|
1028
|
+
return /* @__PURE__ */ jsx(
|
|
1029
|
+
Comp,
|
|
1030
|
+
{
|
|
1031
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
1032
|
+
ref,
|
|
1033
|
+
...props
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
);
|
|
1038
|
+
Button.displayName = "Button";
|
|
1039
|
+
var badgeVariants = cva(
|
|
1040
|
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
1041
|
+
{
|
|
1042
|
+
variants: {
|
|
1043
|
+
variant: {
|
|
1044
|
+
default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
1045
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
1046
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
1047
|
+
outline: "text-foreground"
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
defaultVariants: {
|
|
1051
|
+
variant: "default"
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
function Badge({ className, variant, ...props }) {
|
|
1056
|
+
return /* @__PURE__ */ jsx("div", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
1057
|
+
}
|
|
1058
|
+
var Input = React5.forwardRef(
|
|
1059
|
+
({ className, type, ...props }, ref) => {
|
|
1060
|
+
return /* @__PURE__ */ jsx(
|
|
1061
|
+
"input",
|
|
1062
|
+
{
|
|
1063
|
+
type,
|
|
1064
|
+
className: cn(
|
|
1065
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
1066
|
+
className
|
|
1067
|
+
),
|
|
1068
|
+
ref,
|
|
1069
|
+
...props
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
);
|
|
1074
|
+
Input.displayName = "Input";
|
|
1075
|
+
var labelVariants = cva(
|
|
1076
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
1077
|
+
);
|
|
1078
|
+
var Label = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1079
|
+
LabelPrimitive.Root,
|
|
1080
|
+
{
|
|
1081
|
+
ref,
|
|
1082
|
+
className: cn(labelVariants(), className),
|
|
1083
|
+
...props
|
|
1084
|
+
}
|
|
1085
|
+
));
|
|
1086
|
+
Label.displayName = LabelPrimitive.Root.displayName;
|
|
1087
|
+
var Select = SelectPrimitive.Root;
|
|
1088
|
+
var SelectGroup = SelectPrimitive.Group;
|
|
1089
|
+
var SelectValue = SelectPrimitive.Value;
|
|
1090
|
+
var SelectTrigger = React5.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
1091
|
+
SelectPrimitive.Trigger,
|
|
1092
|
+
{
|
|
1093
|
+
ref,
|
|
1094
|
+
className: cn(
|
|
1095
|
+
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
1096
|
+
className
|
|
1097
|
+
),
|
|
1098
|
+
...props,
|
|
1099
|
+
children: [
|
|
1100
|
+
children,
|
|
1101
|
+
/* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 opacity-50" }) })
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
));
|
|
1105
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
1106
|
+
var SelectScrollUpButton = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1107
|
+
SelectPrimitive.ScrollUpButton,
|
|
1108
|
+
{
|
|
1109
|
+
ref,
|
|
1110
|
+
className: cn(
|
|
1111
|
+
"flex cursor-default items-center justify-center py-1",
|
|
1112
|
+
className
|
|
1113
|
+
),
|
|
1114
|
+
...props,
|
|
1115
|
+
children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4" })
|
|
1116
|
+
}
|
|
1117
|
+
));
|
|
1118
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
1119
|
+
var SelectScrollDownButton = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1120
|
+
SelectPrimitive.ScrollDownButton,
|
|
1121
|
+
{
|
|
1122
|
+
ref,
|
|
1123
|
+
className: cn(
|
|
1124
|
+
"flex cursor-default items-center justify-center py-1",
|
|
1125
|
+
className
|
|
1126
|
+
),
|
|
1127
|
+
...props,
|
|
1128
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" })
|
|
1129
|
+
}
|
|
1130
|
+
));
|
|
1131
|
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
1132
|
+
var SelectContent = React5.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
|
|
1133
|
+
SelectPrimitive.Content,
|
|
1134
|
+
{
|
|
1135
|
+
ref,
|
|
1136
|
+
className: cn(
|
|
1137
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
1138
|
+
position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
1139
|
+
className
|
|
1140
|
+
),
|
|
1141
|
+
position,
|
|
1142
|
+
...props,
|
|
1143
|
+
children: [
|
|
1144
|
+
/* @__PURE__ */ jsx(SelectScrollUpButton, {}),
|
|
1145
|
+
/* @__PURE__ */ jsx(
|
|
1146
|
+
SelectPrimitive.Viewport,
|
|
1147
|
+
{
|
|
1148
|
+
className: cn(
|
|
1149
|
+
"p-1",
|
|
1150
|
+
position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
1151
|
+
),
|
|
1152
|
+
children
|
|
1153
|
+
}
|
|
1154
|
+
),
|
|
1155
|
+
/* @__PURE__ */ jsx(SelectScrollDownButton, {})
|
|
1156
|
+
]
|
|
1157
|
+
}
|
|
1158
|
+
) }));
|
|
1159
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
1160
|
+
var SelectLabel = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1161
|
+
SelectPrimitive.Label,
|
|
1162
|
+
{
|
|
1163
|
+
ref,
|
|
1164
|
+
className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className),
|
|
1165
|
+
...props
|
|
1166
|
+
}
|
|
1167
|
+
));
|
|
1168
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
1169
|
+
var SelectItem = React5.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
1170
|
+
SelectPrimitive.Item,
|
|
1171
|
+
{
|
|
1172
|
+
ref,
|
|
1173
|
+
className: cn(
|
|
1174
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1175
|
+
className
|
|
1176
|
+
),
|
|
1177
|
+
...props,
|
|
1178
|
+
children: [
|
|
1179
|
+
/* @__PURE__ */ jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) }) }),
|
|
1180
|
+
/* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
|
|
1181
|
+
]
|
|
1182
|
+
}
|
|
1183
|
+
));
|
|
1184
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
1185
|
+
var SelectSeparator = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1186
|
+
SelectPrimitive.Separator,
|
|
1187
|
+
{
|
|
1188
|
+
ref,
|
|
1189
|
+
className: cn("-mx-1 my-1 h-px bg-muted", className),
|
|
1190
|
+
...props
|
|
1191
|
+
}
|
|
1192
|
+
));
|
|
1193
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
1194
|
+
function TableFilters({ config, filters, className, defaultExpanded = false }) {
|
|
1195
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
1196
|
+
const [expandedSections, setExpandedSections] = useState(/* @__PURE__ */ new Set());
|
|
1197
|
+
const toggleSection = (sectionId) => {
|
|
1198
|
+
setExpandedSections((prev) => {
|
|
1199
|
+
const next = new Set(prev);
|
|
1200
|
+
if (next.has(sectionId)) {
|
|
1201
|
+
next.delete(sectionId);
|
|
1202
|
+
} else {
|
|
1203
|
+
next.add(sectionId);
|
|
1204
|
+
}
|
|
1205
|
+
return next;
|
|
1206
|
+
});
|
|
1207
|
+
};
|
|
1208
|
+
const renderChipsFilter = (section) => {
|
|
1209
|
+
if (section.type !== "chips" || !section.filters) return null;
|
|
1210
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-3", children: section.filters.map((filter) => {
|
|
1211
|
+
if (!filter.options) return null;
|
|
1212
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1213
|
+
/* @__PURE__ */ jsxs(Label, { className: "text-sm text-muted-foreground whitespace-nowrap", children: [
|
|
1214
|
+
filter.label,
|
|
1215
|
+
":"
|
|
1216
|
+
] }),
|
|
1217
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1", children: filter.options.map((option) => {
|
|
1218
|
+
const isActive = filters.filters[filter.id] === option;
|
|
1219
|
+
return /* @__PURE__ */ jsx(
|
|
1220
|
+
Button,
|
|
1221
|
+
{
|
|
1222
|
+
variant: isActive ? "default" : "outline",
|
|
1223
|
+
size: "sm",
|
|
1224
|
+
onClick: () => {
|
|
1225
|
+
if (isActive) {
|
|
1226
|
+
filters.clearFilter(filter.id);
|
|
1227
|
+
} else {
|
|
1228
|
+
filters.setFilter(filter.id, option);
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
className: "h-7 px-2 text-xs",
|
|
1232
|
+
children: option
|
|
1233
|
+
},
|
|
1234
|
+
option
|
|
1235
|
+
);
|
|
1236
|
+
}) })
|
|
1237
|
+
] }, filter.id);
|
|
1238
|
+
}) });
|
|
1239
|
+
};
|
|
1240
|
+
const renderBucketsFilter = (section) => {
|
|
1241
|
+
if (section.type !== "buckets" || !section.buckets) return null;
|
|
1242
|
+
const currentValue = filters.filters[section.id] || "all";
|
|
1243
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1244
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: section.id, className: "text-xs text-muted-foreground whitespace-nowrap", children: [
|
|
1245
|
+
section.label,
|
|
1246
|
+
":"
|
|
1247
|
+
] }),
|
|
1248
|
+
/* @__PURE__ */ jsxs(Select, { value: currentValue, onValueChange: (value) => filters.setFilter(section.id, value), children: [
|
|
1249
|
+
/* @__PURE__ */ jsx(SelectTrigger, { id: section.id, className: "w-32 h-8 text-xs", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select range" }) }),
|
|
1250
|
+
/* @__PURE__ */ jsx(SelectContent, { children: section.buckets.map((bucket, idx) => /* @__PURE__ */ jsx(SelectItem, { value: bucket.label.toLowerCase().replace(/\s+/g, "-"), children: bucket.label }, idx)) })
|
|
1251
|
+
] })
|
|
1252
|
+
] });
|
|
1253
|
+
};
|
|
1254
|
+
const renderDropdownFilter = (filter) => {
|
|
1255
|
+
if (!filter.options) return null;
|
|
1256
|
+
const currentValue = filters.filters[filter.id] || "all";
|
|
1257
|
+
const filteredOptions = filter.options.filter((option) => option.toLowerCase() !== "all");
|
|
1258
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1259
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: filter.id, className: "text-xs text-muted-foreground whitespace-nowrap", children: [
|
|
1260
|
+
filter.label,
|
|
1261
|
+
":"
|
|
1262
|
+
] }),
|
|
1263
|
+
/* @__PURE__ */ jsxs(Select, { value: currentValue, onValueChange: (value) => filters.setFilter(filter.id, value), children: [
|
|
1264
|
+
/* @__PURE__ */ jsx(SelectTrigger, { id: filter.id, className: "w-28 h-8 text-xs", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "All" }) }),
|
|
1265
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
1266
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "all", children: "All" }),
|
|
1267
|
+
filteredOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option, children: option === "null" ? "Not Set" : option }, option))
|
|
1268
|
+
] })
|
|
1269
|
+
] })
|
|
1270
|
+
] }, filter.id);
|
|
1271
|
+
};
|
|
1272
|
+
const renderSearchFilter = (filter) => {
|
|
1273
|
+
const currentValue = filters.filters[filter.id] || "";
|
|
1274
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1275
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: filter.id, className: "text-xs text-muted-foreground whitespace-nowrap", children: [
|
|
1276
|
+
filter.label,
|
|
1277
|
+
":"
|
|
1278
|
+
] }),
|
|
1279
|
+
/* @__PURE__ */ jsx(
|
|
1280
|
+
Input,
|
|
1281
|
+
{
|
|
1282
|
+
id: filter.id,
|
|
1283
|
+
type: "text",
|
|
1284
|
+
placeholder: filter.label,
|
|
1285
|
+
value: currentValue,
|
|
1286
|
+
onChange: (e) => filters.setFilter(filter.id, e.target.value),
|
|
1287
|
+
className: "w-32 h-8 text-xs"
|
|
1288
|
+
}
|
|
1289
|
+
)
|
|
1290
|
+
] }, filter.id);
|
|
1291
|
+
};
|
|
1292
|
+
const renderCollapsibleSection = (section) => {
|
|
1293
|
+
if (section.type !== "collapsible" || !section.filters) return null;
|
|
1294
|
+
const isExpanded2 = expandedSections.has(section.id);
|
|
1295
|
+
return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg p-3", children: [
|
|
1296
|
+
/* @__PURE__ */ jsxs(
|
|
1297
|
+
Button,
|
|
1298
|
+
{
|
|
1299
|
+
variant: "ghost",
|
|
1300
|
+
size: "sm",
|
|
1301
|
+
onClick: () => toggleSection(section.id),
|
|
1302
|
+
className: "w-full justify-between -ml-2",
|
|
1303
|
+
children: [
|
|
1304
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: section.label }),
|
|
1305
|
+
isExpanded2 ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" })
|
|
1306
|
+
]
|
|
1307
|
+
}
|
|
1308
|
+
),
|
|
1309
|
+
isExpanded2 && /* @__PURE__ */ jsx("div", { className: "mt-3 space-y-3", children: section.filters.map((filter) => {
|
|
1310
|
+
if (filter.type === "dropdown") {
|
|
1311
|
+
return renderDropdownFilter(filter);
|
|
1312
|
+
}
|
|
1313
|
+
if (filter.type === "search") {
|
|
1314
|
+
return renderSearchFilter(filter);
|
|
1315
|
+
}
|
|
1316
|
+
return null;
|
|
1317
|
+
}) })
|
|
1318
|
+
] }, section.id);
|
|
1319
|
+
};
|
|
1320
|
+
const renderCustomFilter = useCallback((section) => {
|
|
1321
|
+
if (section.type !== "custom" || !section.customFilter) return null;
|
|
1322
|
+
const currentValue = filters.filters[section.id] ?? section.customFilter.defaultValue;
|
|
1323
|
+
const onChange = (value) => {
|
|
1324
|
+
if (value === section.customFilter?.defaultValue || value === void 0 || value === null) {
|
|
1325
|
+
filters.clearFilter(section.id);
|
|
1326
|
+
} else {
|
|
1327
|
+
filters.setFilter(section.id, value);
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1331
|
+
section.label && /* @__PURE__ */ jsxs(Label, { className: "text-xs text-muted-foreground whitespace-nowrap", children: [
|
|
1332
|
+
section.label,
|
|
1333
|
+
":"
|
|
1334
|
+
] }),
|
|
1335
|
+
section.customFilter.render(currentValue, onChange)
|
|
1336
|
+
] }, section.id);
|
|
1337
|
+
}, [filters]);
|
|
1338
|
+
const hasActiveFilters = filters.hasActiveFilters();
|
|
1339
|
+
const activeFilterCount = filters.getActiveFilterCount();
|
|
1340
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("rounded-lg border bg-muted/30", className), children: [
|
|
1341
|
+
/* @__PURE__ */ jsxs(
|
|
1342
|
+
"button",
|
|
1343
|
+
{
|
|
1344
|
+
type: "button",
|
|
1345
|
+
onClick: () => setIsExpanded(!isExpanded),
|
|
1346
|
+
className: "w-full flex items-center justify-between px-3 py-2 hover:bg-muted/50 transition-colors",
|
|
1347
|
+
children: [
|
|
1348
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1349
|
+
/* @__PURE__ */ jsx(Filter, { className: "h-4 w-4 text-muted-foreground" }),
|
|
1350
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Filters" }),
|
|
1351
|
+
hasActiveFilters && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: activeFilterCount })
|
|
1352
|
+
] }),
|
|
1353
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1354
|
+
hasActiveFilters && /* @__PURE__ */ jsxs(
|
|
1355
|
+
Button,
|
|
1356
|
+
{
|
|
1357
|
+
variant: "ghost",
|
|
1358
|
+
size: "sm",
|
|
1359
|
+
onClick: (e) => {
|
|
1360
|
+
e.stopPropagation();
|
|
1361
|
+
filters.clearAllFilters();
|
|
1362
|
+
},
|
|
1363
|
+
className: "gap-1 h-6 px-2 text-xs",
|
|
1364
|
+
children: [
|
|
1365
|
+
/* @__PURE__ */ jsx(XCircle, { className: "h-3 w-3" }),
|
|
1366
|
+
"Clear"
|
|
1367
|
+
]
|
|
1368
|
+
}
|
|
1369
|
+
),
|
|
1370
|
+
isExpanded ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4 text-muted-foreground" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" })
|
|
1371
|
+
] })
|
|
1372
|
+
]
|
|
1373
|
+
}
|
|
1374
|
+
),
|
|
1375
|
+
isExpanded && /* @__PURE__ */ jsx("div", { className: "px-3 pb-3 pt-1", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2", children: config.sections.map((section) => {
|
|
1376
|
+
if (section.type === "chips") {
|
|
1377
|
+
return /* @__PURE__ */ jsx("div", { children: renderChipsFilter(section) }, section.id);
|
|
1378
|
+
}
|
|
1379
|
+
if (section.type === "buckets") {
|
|
1380
|
+
return /* @__PURE__ */ jsx("div", { children: renderBucketsFilter(section) }, section.id);
|
|
1381
|
+
}
|
|
1382
|
+
if (section.type === "collapsible") {
|
|
1383
|
+
return renderCollapsibleSection(section);
|
|
1384
|
+
}
|
|
1385
|
+
if (section.type === "dropdown" && section.filters) {
|
|
1386
|
+
return section.filters.map((filter) => /* @__PURE__ */ jsx("div", { children: renderDropdownFilter(filter) }, filter.id));
|
|
1387
|
+
}
|
|
1388
|
+
if (section.type === "search" && section.filters) {
|
|
1389
|
+
return section.filters.map((filter) => /* @__PURE__ */ jsx("div", { children: renderSearchFilter(filter) }, filter.id));
|
|
1390
|
+
}
|
|
1391
|
+
if (section.type === "custom") {
|
|
1392
|
+
return /* @__PURE__ */ jsx("div", { children: renderCustomFilter(section) }, section.id);
|
|
1393
|
+
}
|
|
1394
|
+
return null;
|
|
1395
|
+
}) }) })
|
|
1396
|
+
] });
|
|
1397
|
+
}
|
|
1398
|
+
function BulkActionBar({
|
|
1399
|
+
selectedCount,
|
|
1400
|
+
selectAllPages,
|
|
1401
|
+
totalCount,
|
|
1402
|
+
bulkActions,
|
|
1403
|
+
onClearSelection,
|
|
1404
|
+
onExecuteAction,
|
|
1405
|
+
className
|
|
1406
|
+
}) {
|
|
1407
|
+
const isVisible = selectedCount > 0;
|
|
1408
|
+
const getVariantClassName = (variant) => {
|
|
1409
|
+
switch (variant) {
|
|
1410
|
+
case "gradient-purple":
|
|
1411
|
+
return "bg-gradient-to-r from-purple-600 to-blue-600 text-white hover:from-purple-700 hover:to-blue-700";
|
|
1412
|
+
case "gradient-green":
|
|
1413
|
+
return "bg-gradient-to-r from-green-600 to-teal-600 text-white hover:from-green-700 hover:to-teal-700";
|
|
1414
|
+
case "gradient-indigo":
|
|
1415
|
+
return "bg-gradient-to-r from-indigo-600 to-purple-600 text-white hover:from-indigo-700 hover:to-purple-700";
|
|
1416
|
+
case "gradient-orange":
|
|
1417
|
+
return "bg-gradient-to-r from-orange-600 to-red-600 text-white hover:from-orange-700 hover:to-red-700";
|
|
1418
|
+
case "gradient-blue":
|
|
1419
|
+
return "bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700";
|
|
1420
|
+
default:
|
|
1421
|
+
return "";
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
return /* @__PURE__ */ jsx(
|
|
1425
|
+
"div",
|
|
1426
|
+
{
|
|
1427
|
+
className: cn(
|
|
1428
|
+
"transition-all duration-200 ease-in-out overflow-hidden",
|
|
1429
|
+
isVisible ? "opacity-100 max-h-20" : "opacity-0 max-h-0 pointer-events-none",
|
|
1430
|
+
className
|
|
1431
|
+
),
|
|
1432
|
+
"aria-hidden": !isVisible,
|
|
1433
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 bg-muted/50 rounded-lg border", children: [
|
|
1434
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 text-sm font-medium", children: selectAllPages ? /* @__PURE__ */ jsxs("span", { className: "text-blue-600", children: [
|
|
1435
|
+
"All ",
|
|
1436
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: totalCount }),
|
|
1437
|
+
" items selected"
|
|
1438
|
+
] }) : /* @__PURE__ */ jsx("span", { children: `${selectedCount} ${selectedCount === 1 ? "item" : "items"} selected` }) }),
|
|
1439
|
+
/* @__PURE__ */ jsx("div", { className: "h-6 w-px bg-border mx-1" }),
|
|
1440
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 flex-1", children: bulkActions.map((action) => {
|
|
1441
|
+
const Icon2 = action.icon;
|
|
1442
|
+
const isDisabled = action.disabled?.(/* @__PURE__ */ new Set()) || false;
|
|
1443
|
+
const exceedsMaxSelection = !!(action.maxSelection && selectedCount > action.maxSelection);
|
|
1444
|
+
return /* @__PURE__ */ jsxs(
|
|
1445
|
+
Button,
|
|
1446
|
+
{
|
|
1447
|
+
size: "sm",
|
|
1448
|
+
variant: action.variant === "default" ? "default" : "default",
|
|
1449
|
+
className: cn(
|
|
1450
|
+
getVariantClassName(action.variant),
|
|
1451
|
+
"gap-2"
|
|
1452
|
+
),
|
|
1453
|
+
onClick: () => onExecuteAction(action),
|
|
1454
|
+
disabled: isDisabled || exceedsMaxSelection,
|
|
1455
|
+
title: exceedsMaxSelection ? `Maximum ${action.maxSelection} items can be selected for this action` : action.label,
|
|
1456
|
+
children: [
|
|
1457
|
+
/* @__PURE__ */ jsx(Icon2, { className: "h-4 w-4" }),
|
|
1458
|
+
action.label,
|
|
1459
|
+
selectAllPages && totalCount > 0 && ` (${totalCount})`
|
|
1460
|
+
]
|
|
1461
|
+
},
|
|
1462
|
+
action.id
|
|
1463
|
+
);
|
|
1464
|
+
}) }),
|
|
1465
|
+
/* @__PURE__ */ jsxs(
|
|
1466
|
+
Button,
|
|
1467
|
+
{
|
|
1468
|
+
variant: "outline",
|
|
1469
|
+
size: "sm",
|
|
1470
|
+
onClick: onClearSelection,
|
|
1471
|
+
className: "gap-2",
|
|
1472
|
+
children: [
|
|
1473
|
+
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
1474
|
+
"Clear"
|
|
1475
|
+
]
|
|
1476
|
+
}
|
|
1477
|
+
)
|
|
1478
|
+
] })
|
|
1479
|
+
}
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
var Table = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsx(
|
|
1483
|
+
"table",
|
|
1484
|
+
{
|
|
1485
|
+
ref,
|
|
1486
|
+
className: cn("w-full caption-bottom text-sm", className),
|
|
1487
|
+
...props
|
|
1488
|
+
}
|
|
1489
|
+
) }));
|
|
1490
|
+
Table.displayName = "Table";
|
|
1491
|
+
var TableHeader = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("thead", { ref, className: cn("[&_tr]:border-b", className), ...props }));
|
|
1492
|
+
TableHeader.displayName = "TableHeader";
|
|
1493
|
+
var TableBody = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1494
|
+
"tbody",
|
|
1495
|
+
{
|
|
1496
|
+
ref,
|
|
1497
|
+
className: cn("[&_tr:last-child]:border-0", className),
|
|
1498
|
+
...props
|
|
1499
|
+
}
|
|
1500
|
+
));
|
|
1501
|
+
TableBody.displayName = "TableBody";
|
|
1502
|
+
var TableFooter = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1503
|
+
"tfoot",
|
|
1504
|
+
{
|
|
1505
|
+
ref,
|
|
1506
|
+
className: cn(
|
|
1507
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
1508
|
+
className
|
|
1509
|
+
),
|
|
1510
|
+
...props
|
|
1511
|
+
}
|
|
1512
|
+
));
|
|
1513
|
+
TableFooter.displayName = "TableFooter";
|
|
1514
|
+
var TableRow = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1515
|
+
"tr",
|
|
1516
|
+
{
|
|
1517
|
+
ref,
|
|
1518
|
+
className: cn(
|
|
1519
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
1520
|
+
className
|
|
1521
|
+
),
|
|
1522
|
+
...props
|
|
1523
|
+
}
|
|
1524
|
+
));
|
|
1525
|
+
TableRow.displayName = "TableRow";
|
|
1526
|
+
var TableHead = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1527
|
+
"th",
|
|
1528
|
+
{
|
|
1529
|
+
ref,
|
|
1530
|
+
className: cn(
|
|
1531
|
+
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
1532
|
+
className
|
|
1533
|
+
),
|
|
1534
|
+
...props
|
|
1535
|
+
}
|
|
1536
|
+
));
|
|
1537
|
+
TableHead.displayName = "TableHead";
|
|
1538
|
+
var TableCell = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1539
|
+
"td",
|
|
1540
|
+
{
|
|
1541
|
+
ref,
|
|
1542
|
+
className: cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className),
|
|
1543
|
+
...props
|
|
1544
|
+
}
|
|
1545
|
+
));
|
|
1546
|
+
TableCell.displayName = "TableCell";
|
|
1547
|
+
var TableCaption = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1548
|
+
"caption",
|
|
1549
|
+
{
|
|
1550
|
+
ref,
|
|
1551
|
+
className: cn("mt-4 text-sm text-muted-foreground", className),
|
|
1552
|
+
...props
|
|
1553
|
+
}
|
|
1554
|
+
));
|
|
1555
|
+
TableCaption.displayName = "TableCaption";
|
|
1556
|
+
var Checkbox = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1557
|
+
CheckboxPrimitive.Root,
|
|
1558
|
+
{
|
|
1559
|
+
ref,
|
|
1560
|
+
className: cn(
|
|
1561
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
1562
|
+
className
|
|
1563
|
+
),
|
|
1564
|
+
...props,
|
|
1565
|
+
children: /* @__PURE__ */ jsx(
|
|
1566
|
+
CheckboxPrimitive.Indicator,
|
|
1567
|
+
{
|
|
1568
|
+
className: cn("flex items-center justify-center text-current"),
|
|
1569
|
+
children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
|
|
1570
|
+
}
|
|
1571
|
+
)
|
|
1572
|
+
}
|
|
1573
|
+
));
|
|
1574
|
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
1575
|
+
var DropdownMenu = DropdownMenuPrimitive.Root;
|
|
1576
|
+
var DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
1577
|
+
var DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
1578
|
+
var DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
1579
|
+
var DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
1580
|
+
var DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
1581
|
+
var DropdownMenuSubTrigger = React5.forwardRef(({ className, inset, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
1582
|
+
DropdownMenuPrimitive.SubTrigger,
|
|
1583
|
+
{
|
|
1584
|
+
ref,
|
|
1585
|
+
className: cn(
|
|
1586
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
|
1587
|
+
inset && "pl-8",
|
|
1588
|
+
className
|
|
1589
|
+
),
|
|
1590
|
+
...props,
|
|
1591
|
+
children: [
|
|
1592
|
+
children,
|
|
1593
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "ml-auto h-4 w-4" })
|
|
1594
|
+
]
|
|
1595
|
+
}
|
|
1596
|
+
));
|
|
1597
|
+
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
|
1598
|
+
var DropdownMenuSubContent = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1599
|
+
DropdownMenuPrimitive.SubContent,
|
|
1600
|
+
{
|
|
1601
|
+
ref,
|
|
1602
|
+
className: cn(
|
|
1603
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
1604
|
+
className
|
|
1605
|
+
),
|
|
1606
|
+
...props
|
|
1607
|
+
}
|
|
1608
|
+
));
|
|
1609
|
+
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
|
1610
|
+
var DropdownMenuContent = React5.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
1611
|
+
DropdownMenuPrimitive.Content,
|
|
1612
|
+
{
|
|
1613
|
+
ref,
|
|
1614
|
+
sideOffset,
|
|
1615
|
+
className: cn(
|
|
1616
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
1617
|
+
className
|
|
1618
|
+
),
|
|
1619
|
+
...props
|
|
1620
|
+
}
|
|
1621
|
+
) }));
|
|
1622
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
1623
|
+
var DropdownMenuItem = React5.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1624
|
+
DropdownMenuPrimitive.Item,
|
|
1625
|
+
{
|
|
1626
|
+
ref,
|
|
1627
|
+
className: cn(
|
|
1628
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1629
|
+
inset && "pl-8",
|
|
1630
|
+
className
|
|
1631
|
+
),
|
|
1632
|
+
...props
|
|
1633
|
+
}
|
|
1634
|
+
));
|
|
1635
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
1636
|
+
var DropdownMenuCheckboxItem = React5.forwardRef(({ className, children, checked, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
1637
|
+
DropdownMenuPrimitive.CheckboxItem,
|
|
1638
|
+
{
|
|
1639
|
+
ref,
|
|
1640
|
+
className: cn(
|
|
1641
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1642
|
+
className
|
|
1643
|
+
),
|
|
1644
|
+
checked,
|
|
1645
|
+
...props,
|
|
1646
|
+
children: [
|
|
1647
|
+
/* @__PURE__ */ jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(DropdownMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) }) }),
|
|
1648
|
+
children
|
|
1649
|
+
]
|
|
1650
|
+
}
|
|
1651
|
+
));
|
|
1652
|
+
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
1653
|
+
var DropdownMenuRadioItem = React5.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
1654
|
+
DropdownMenuPrimitive.RadioItem,
|
|
1655
|
+
{
|
|
1656
|
+
ref,
|
|
1657
|
+
className: cn(
|
|
1658
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1659
|
+
className
|
|
1660
|
+
),
|
|
1661
|
+
...props,
|
|
1662
|
+
children: [
|
|
1663
|
+
/* @__PURE__ */ jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(DropdownMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Circle, { className: "h-2 w-2 fill-current" }) }) }),
|
|
1664
|
+
children
|
|
1665
|
+
]
|
|
1666
|
+
}
|
|
1667
|
+
));
|
|
1668
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
1669
|
+
var DropdownMenuLabel = React5.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1670
|
+
DropdownMenuPrimitive.Label,
|
|
1671
|
+
{
|
|
1672
|
+
ref,
|
|
1673
|
+
className: cn(
|
|
1674
|
+
"px-2 py-1.5 text-sm font-semibold",
|
|
1675
|
+
inset && "pl-8",
|
|
1676
|
+
className
|
|
1677
|
+
),
|
|
1678
|
+
...props
|
|
1679
|
+
}
|
|
1680
|
+
));
|
|
1681
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
1682
|
+
var DropdownMenuSeparator = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1683
|
+
DropdownMenuPrimitive.Separator,
|
|
1684
|
+
{
|
|
1685
|
+
ref,
|
|
1686
|
+
className: cn("-mx-1 my-1 h-px bg-muted", className),
|
|
1687
|
+
...props
|
|
1688
|
+
}
|
|
1689
|
+
));
|
|
1690
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
1691
|
+
var DropdownMenuShortcut = ({
|
|
1692
|
+
className,
|
|
1693
|
+
...props
|
|
1694
|
+
}) => {
|
|
1695
|
+
return /* @__PURE__ */ jsx(
|
|
1696
|
+
"span",
|
|
1697
|
+
{
|
|
1698
|
+
className: cn("ml-auto text-xs tracking-widest opacity-60", className),
|
|
1699
|
+
...props
|
|
1700
|
+
}
|
|
1701
|
+
);
|
|
1702
|
+
};
|
|
1703
|
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
|
1704
|
+
function InlineEditCell({
|
|
1705
|
+
value: initialValue,
|
|
1706
|
+
columnId,
|
|
1707
|
+
rowId,
|
|
1708
|
+
editType = "text",
|
|
1709
|
+
editOptions = [],
|
|
1710
|
+
onSave,
|
|
1711
|
+
onCancel,
|
|
1712
|
+
validate
|
|
1713
|
+
}) {
|
|
1714
|
+
const [value, setValue] = useState(initialValue ?? "");
|
|
1715
|
+
const [error, setError] = useState(null);
|
|
1716
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
1717
|
+
const inputRef = useRef(null);
|
|
1718
|
+
useEffect(() => {
|
|
1719
|
+
if (inputRef.current) {
|
|
1720
|
+
inputRef.current.focus();
|
|
1721
|
+
inputRef.current.select();
|
|
1722
|
+
}
|
|
1723
|
+
}, []);
|
|
1724
|
+
const handleSave = useCallback(async () => {
|
|
1725
|
+
if (validate) {
|
|
1726
|
+
const validationError = validate(value);
|
|
1727
|
+
if (validationError) {
|
|
1728
|
+
setError(validationError);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
setIsSaving(true);
|
|
1733
|
+
setError(null);
|
|
1734
|
+
try {
|
|
1735
|
+
await onSave(value);
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
setError(err instanceof Error ? err.message : "Failed to save");
|
|
1738
|
+
setIsSaving(false);
|
|
1739
|
+
}
|
|
1740
|
+
}, [value, validate, onSave]);
|
|
1741
|
+
const handleKeyDown = useCallback((e) => {
|
|
1742
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1743
|
+
e.preventDefault();
|
|
1744
|
+
handleSave();
|
|
1745
|
+
} else if (e.key === "Escape") {
|
|
1746
|
+
e.preventDefault();
|
|
1747
|
+
onCancel();
|
|
1748
|
+
}
|
|
1749
|
+
}, [handleSave, onCancel]);
|
|
1750
|
+
if (editType === "select") {
|
|
1751
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1752
|
+
/* @__PURE__ */ jsxs(
|
|
1753
|
+
Select,
|
|
1754
|
+
{
|
|
1755
|
+
value: String(value),
|
|
1756
|
+
onValueChange: (newValue) => {
|
|
1757
|
+
setValue(newValue);
|
|
1758
|
+
onSave(newValue);
|
|
1759
|
+
},
|
|
1760
|
+
children: [
|
|
1761
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-full", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
1762
|
+
/* @__PURE__ */ jsx(SelectContent, { children: editOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option, children: option }, option)) })
|
|
1763
|
+
]
|
|
1764
|
+
}
|
|
1765
|
+
),
|
|
1766
|
+
/* @__PURE__ */ jsx(
|
|
1767
|
+
"button",
|
|
1768
|
+
{
|
|
1769
|
+
onClick: onCancel,
|
|
1770
|
+
className: "p-1 hover:bg-muted rounded",
|
|
1771
|
+
disabled: isSaving,
|
|
1772
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4 text-muted-foreground" })
|
|
1773
|
+
}
|
|
1774
|
+
)
|
|
1775
|
+
] });
|
|
1776
|
+
}
|
|
1777
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
1778
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1779
|
+
/* @__PURE__ */ jsx(
|
|
1780
|
+
Input,
|
|
1781
|
+
{
|
|
1782
|
+
ref: inputRef,
|
|
1783
|
+
type: editType === "number" ? "number" : editType === "date" ? "date" : "text",
|
|
1784
|
+
value,
|
|
1785
|
+
onChange: (e) => {
|
|
1786
|
+
setValue(e.target.value);
|
|
1787
|
+
setError(null);
|
|
1788
|
+
},
|
|
1789
|
+
onKeyDown: handleKeyDown,
|
|
1790
|
+
className: cn(
|
|
1791
|
+
"h-8 w-full",
|
|
1792
|
+
error && "border-red-500 focus-visible:ring-red-500"
|
|
1793
|
+
),
|
|
1794
|
+
disabled: isSaving
|
|
1795
|
+
}
|
|
1796
|
+
),
|
|
1797
|
+
/* @__PURE__ */ jsx(
|
|
1798
|
+
"button",
|
|
1799
|
+
{
|
|
1800
|
+
onClick: handleSave,
|
|
1801
|
+
className: "p-1 hover:bg-green-100 rounded",
|
|
1802
|
+
disabled: isSaving,
|
|
1803
|
+
children: isSaving ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" }) : /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-green-600" })
|
|
1804
|
+
}
|
|
1805
|
+
),
|
|
1806
|
+
/* @__PURE__ */ jsx(
|
|
1807
|
+
"button",
|
|
1808
|
+
{
|
|
1809
|
+
onClick: onCancel,
|
|
1810
|
+
className: "p-1 hover:bg-red-100 rounded",
|
|
1811
|
+
disabled: isSaving,
|
|
1812
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4 text-red-600" })
|
|
1813
|
+
}
|
|
1814
|
+
)
|
|
1815
|
+
] }),
|
|
1816
|
+
error && /* @__PURE__ */ jsx("span", { className: "text-xs text-red-500", children: error })
|
|
1817
|
+
] });
|
|
1818
|
+
}
|
|
1819
|
+
function DataTableCore({
|
|
1820
|
+
data,
|
|
1821
|
+
columns,
|
|
1822
|
+
getRowId,
|
|
1823
|
+
selectionEnabled,
|
|
1824
|
+
selectedIds = /* @__PURE__ */ new Set(),
|
|
1825
|
+
onToggleRow,
|
|
1826
|
+
onToggleAll,
|
|
1827
|
+
allRowsSelected,
|
|
1828
|
+
renderSelectionCell,
|
|
1829
|
+
sortingEnabled,
|
|
1830
|
+
sortBy,
|
|
1831
|
+
sortDirection,
|
|
1832
|
+
onSort,
|
|
1833
|
+
columnReorderEnabled,
|
|
1834
|
+
onColumnDragEnd,
|
|
1835
|
+
columnResizeEnabled,
|
|
1836
|
+
columnWidths = {},
|
|
1837
|
+
onColumnResizeStart,
|
|
1838
|
+
isResizing,
|
|
1839
|
+
inlineEditEnabled,
|
|
1840
|
+
onCellEdit,
|
|
1841
|
+
rowActions,
|
|
1842
|
+
onRowClick,
|
|
1843
|
+
loadingRows = /* @__PURE__ */ new Set(),
|
|
1844
|
+
className
|
|
1845
|
+
}) {
|
|
1846
|
+
const [editingCell, setEditingCell] = useState(null);
|
|
1847
|
+
const [isDraggingColumn, setIsDraggingColumn] = useState(false);
|
|
1848
|
+
const handleDragStart = useCallback((_) => {
|
|
1849
|
+
setIsDraggingColumn(true);
|
|
1850
|
+
}, []);
|
|
1851
|
+
const handleDragEnd = useCallback((result) => {
|
|
1852
|
+
setIsDraggingColumn(false);
|
|
1853
|
+
onColumnDragEnd?.(result);
|
|
1854
|
+
}, [onColumnDragEnd]);
|
|
1855
|
+
const getEffectiveWidth = (column) => {
|
|
1856
|
+
if (columnResizeEnabled && columnWidths[column.id]) {
|
|
1857
|
+
return columnWidths[column.id];
|
|
1858
|
+
}
|
|
1859
|
+
return column.width;
|
|
1860
|
+
};
|
|
1861
|
+
const handleResizeMouseDown = useCallback((columnId, e) => {
|
|
1862
|
+
e.stopPropagation();
|
|
1863
|
+
e.preventDefault();
|
|
1864
|
+
const th = e.target.closest("th");
|
|
1865
|
+
if (th && onColumnResizeStart) {
|
|
1866
|
+
onColumnResizeStart(columnId, e.clientX, th.offsetWidth);
|
|
1867
|
+
}
|
|
1868
|
+
}, [onColumnResizeStart]);
|
|
1869
|
+
const renderResizeHandle = (columnId) => {
|
|
1870
|
+
if (!columnResizeEnabled) return null;
|
|
1871
|
+
return /* @__PURE__ */ jsx(
|
|
1872
|
+
"div",
|
|
1873
|
+
{
|
|
1874
|
+
"data-resize-handle": "true",
|
|
1875
|
+
className: "absolute right-0 top-0 h-full w-5 cursor-col-resize z-50 flex items-center justify-center group/resize pointer-events-auto",
|
|
1876
|
+
style: { marginRight: "-10px" },
|
|
1877
|
+
onMouseDown: (e) => handleResizeMouseDown(columnId, e),
|
|
1878
|
+
onPointerDown: (e) => handleResizeMouseDown(columnId, e),
|
|
1879
|
+
onDragStart: (e) => e.preventDefault(),
|
|
1880
|
+
children: /* @__PURE__ */ jsx("div", { className: "w-[3px] h-full bg-transparent group-hover/resize:bg-primary transition-colors rounded-full" })
|
|
1881
|
+
}
|
|
1882
|
+
);
|
|
1883
|
+
};
|
|
1884
|
+
const renderCellContent = (column, row) => {
|
|
1885
|
+
if (column.cell) {
|
|
1886
|
+
return column.cell(row);
|
|
1887
|
+
}
|
|
1888
|
+
if (column.accessorFn) {
|
|
1889
|
+
return column.accessorFn(row);
|
|
1890
|
+
}
|
|
1891
|
+
if (column.accessorKey) {
|
|
1892
|
+
const value = row[column.accessorKey];
|
|
1893
|
+
return value !== null && value !== void 0 ? String(value) : "-";
|
|
1894
|
+
}
|
|
1895
|
+
return "-";
|
|
1896
|
+
};
|
|
1897
|
+
const SortIndicator = ({ columnId }) => {
|
|
1898
|
+
const isSorted = sortBy === columnId;
|
|
1899
|
+
const direction = isSorted ? sortDirection : null;
|
|
1900
|
+
if (direction === "asc") {
|
|
1901
|
+
return /* @__PURE__ */ jsx(ArrowUp, { className: "ml-2 h-4 w-4" });
|
|
1902
|
+
} else if (direction === "desc") {
|
|
1903
|
+
return /* @__PURE__ */ jsx(ArrowDown, { className: "ml-2 h-4 w-4" });
|
|
1904
|
+
}
|
|
1905
|
+
return /* @__PURE__ */ jsx(ArrowUpDown, { className: "ml-2 h-4 w-4 opacity-50" });
|
|
1906
|
+
};
|
|
1907
|
+
const handleSortClick = useCallback((columnId) => {
|
|
1908
|
+
if (isDraggingColumn) return;
|
|
1909
|
+
onSort?.(columnId);
|
|
1910
|
+
}, [isDraggingColumn, onSort]);
|
|
1911
|
+
const renderHeader = (column, inDragContext = false) => {
|
|
1912
|
+
if (typeof column.header === "function") {
|
|
1913
|
+
return column.header({});
|
|
1914
|
+
}
|
|
1915
|
+
if (inDragContext) {
|
|
1916
|
+
return /* @__PURE__ */ jsx("span", { children: column.header });
|
|
1917
|
+
}
|
|
1918
|
+
if (sortingEnabled && column.sortable !== false) {
|
|
1919
|
+
return /* @__PURE__ */ jsxs(
|
|
1920
|
+
Button,
|
|
1921
|
+
{
|
|
1922
|
+
variant: "ghost",
|
|
1923
|
+
onClick: () => handleSortClick(column.id),
|
|
1924
|
+
className: "-ml-4 h-8 hover:bg-transparent",
|
|
1925
|
+
children: [
|
|
1926
|
+
column.header,
|
|
1927
|
+
/* @__PURE__ */ jsx(SortIndicator, { columnId: column.id })
|
|
1928
|
+
]
|
|
1929
|
+
}
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
return column.header;
|
|
1933
|
+
};
|
|
1934
|
+
const renderColumnHeaders = () => {
|
|
1935
|
+
if (columnReorderEnabled && onColumnDragEnd) {
|
|
1936
|
+
return /* @__PURE__ */ jsx(DragDropContext, { onDragStart: handleDragStart, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(Droppable, { droppableId: "columns", direction: "horizontal", children: (provided) => /* @__PURE__ */ jsxs(
|
|
1937
|
+
TableRow,
|
|
1938
|
+
{
|
|
1939
|
+
ref: provided.innerRef,
|
|
1940
|
+
...provided.droppableProps,
|
|
1941
|
+
children: [
|
|
1942
|
+
selectionEnabled && /* @__PURE__ */ jsx(TableHead, { className: "w-12", children: /* @__PURE__ */ jsx(
|
|
1943
|
+
Checkbox,
|
|
1944
|
+
{
|
|
1945
|
+
checked: allRowsSelected,
|
|
1946
|
+
onCheckedChange: onToggleAll,
|
|
1947
|
+
"aria-label": "Select all"
|
|
1948
|
+
}
|
|
1949
|
+
) }),
|
|
1950
|
+
columns.map((column, index) => /* @__PURE__ */ jsx(Draggable, { draggableId: column.id, index, children: (provided2, snapshot) => /* @__PURE__ */ jsxs(
|
|
1951
|
+
TableHead,
|
|
1952
|
+
{
|
|
1953
|
+
ref: provided2.innerRef,
|
|
1954
|
+
...provided2.draggableProps,
|
|
1955
|
+
style: {
|
|
1956
|
+
...provided2.draggableProps.style,
|
|
1957
|
+
width: getEffectiveWidth(column),
|
|
1958
|
+
minWidth: column.minWidth,
|
|
1959
|
+
maxWidth: column.maxWidth
|
|
1960
|
+
},
|
|
1961
|
+
className: cn(
|
|
1962
|
+
"relative group overflow-visible",
|
|
1963
|
+
snapshot.isDragging && "bg-primary/10 shadow-lg ring-2 ring-primary/20 z-50",
|
|
1964
|
+
columnResizeEnabled && "select-none"
|
|
1965
|
+
),
|
|
1966
|
+
children: [
|
|
1967
|
+
/* @__PURE__ */ jsxs(
|
|
1968
|
+
"div",
|
|
1969
|
+
{
|
|
1970
|
+
...provided2.dragHandleProps,
|
|
1971
|
+
className: cn(
|
|
1972
|
+
"flex items-center gap-2 cursor-grab active:cursor-grabbing",
|
|
1973
|
+
"hover:bg-muted/50 rounded px-1 -mx-1 py-1 transition-colors",
|
|
1974
|
+
columnResizeEnabled && "pr-4"
|
|
1975
|
+
),
|
|
1976
|
+
children: [
|
|
1977
|
+
/* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4 text-muted-foreground flex-shrink-0" }),
|
|
1978
|
+
/* @__PURE__ */ jsxs(
|
|
1979
|
+
"div",
|
|
1980
|
+
{
|
|
1981
|
+
className: "flex-1 flex items-center",
|
|
1982
|
+
onClick: (e) => {
|
|
1983
|
+
if (!snapshot.isDragging && sortingEnabled && column.sortable !== false) {
|
|
1984
|
+
e.stopPropagation();
|
|
1985
|
+
handleSortClick(column.id);
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1988
|
+
children: [
|
|
1989
|
+
renderHeader(column, true),
|
|
1990
|
+
sortingEnabled && column.sortable !== false && /* @__PURE__ */ jsx(SortIndicator, { columnId: column.id })
|
|
1991
|
+
]
|
|
1992
|
+
}
|
|
1993
|
+
)
|
|
1994
|
+
]
|
|
1995
|
+
}
|
|
1996
|
+
),
|
|
1997
|
+
renderResizeHandle(column.id)
|
|
1998
|
+
]
|
|
1999
|
+
}
|
|
2000
|
+
) }, column.id)),
|
|
2001
|
+
provided.placeholder,
|
|
2002
|
+
rowActions && rowActions.length > 0 && /* @__PURE__ */ jsx(TableHead, { className: "w-12", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Actions" }) })
|
|
2003
|
+
]
|
|
2004
|
+
}
|
|
2005
|
+
) }) }) });
|
|
2006
|
+
}
|
|
2007
|
+
return /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
2008
|
+
selectionEnabled && /* @__PURE__ */ jsx(TableHead, { className: "w-12", children: /* @__PURE__ */ jsx(
|
|
2009
|
+
Checkbox,
|
|
2010
|
+
{
|
|
2011
|
+
checked: allRowsSelected,
|
|
2012
|
+
onCheckedChange: onToggleAll,
|
|
2013
|
+
"aria-label": "Select all"
|
|
2014
|
+
}
|
|
2015
|
+
) }),
|
|
2016
|
+
columns.map((column) => /* @__PURE__ */ jsxs(
|
|
2017
|
+
TableHead,
|
|
2018
|
+
{
|
|
2019
|
+
className: cn("relative group overflow-visible", columnResizeEnabled && "select-none"),
|
|
2020
|
+
style: {
|
|
2021
|
+
width: getEffectiveWidth(column),
|
|
2022
|
+
minWidth: column.minWidth,
|
|
2023
|
+
maxWidth: column.maxWidth
|
|
2024
|
+
},
|
|
2025
|
+
children: [
|
|
2026
|
+
renderHeader(column),
|
|
2027
|
+
renderResizeHandle(column.id)
|
|
2028
|
+
]
|
|
2029
|
+
},
|
|
2030
|
+
column.id
|
|
2031
|
+
)),
|
|
2032
|
+
rowActions && rowActions.length > 0 && /* @__PURE__ */ jsx(TableHead, { className: "w-12", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Actions" }) })
|
|
2033
|
+
] }) });
|
|
2034
|
+
};
|
|
2035
|
+
return /* @__PURE__ */ jsx("div", { className: cn("rounded-md border", className), children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
2036
|
+
renderColumnHeaders(),
|
|
2037
|
+
/* @__PURE__ */ jsx(TableBody, { children: data.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
2038
|
+
TableCell,
|
|
2039
|
+
{
|
|
2040
|
+
colSpan: columns.length + (selectionEnabled ? 1 : 0) + (rowActions ? 1 : 0),
|
|
2041
|
+
className: "h-24 text-center",
|
|
2042
|
+
children: "No results found."
|
|
2043
|
+
}
|
|
2044
|
+
) }) : data.map((row, rowIndex) => {
|
|
2045
|
+
const rowId = getRowId(row);
|
|
2046
|
+
const isSelected = selectedIds.has(rowId);
|
|
2047
|
+
const isLoading = loadingRows.has(rowId);
|
|
2048
|
+
return /* @__PURE__ */ jsxs(
|
|
2049
|
+
TableRow,
|
|
2050
|
+
{
|
|
2051
|
+
"data-row-index": rowIndex,
|
|
2052
|
+
"data-state": isSelected && "selected",
|
|
2053
|
+
onClick: () => onRowClick?.(row),
|
|
2054
|
+
tabIndex: 0,
|
|
2055
|
+
className: cn(
|
|
2056
|
+
onRowClick && "cursor-pointer hover:bg-muted/50",
|
|
2057
|
+
isLoading && "opacity-50"
|
|
2058
|
+
),
|
|
2059
|
+
children: [
|
|
2060
|
+
selectionEnabled && /* @__PURE__ */ jsx(TableCell, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
2061
|
+
/* @__PURE__ */ jsx(
|
|
2062
|
+
Checkbox,
|
|
2063
|
+
{
|
|
2064
|
+
checked: isSelected,
|
|
2065
|
+
onCheckedChange: () => onToggleRow?.(rowId),
|
|
2066
|
+
"aria-label": "Select row"
|
|
2067
|
+
}
|
|
2068
|
+
),
|
|
2069
|
+
renderSelectionCell?.(row)
|
|
2070
|
+
] }) }),
|
|
2071
|
+
columns.map((column) => {
|
|
2072
|
+
const isEditing = editingCell?.rowId === rowId && editingCell?.columnId === column.id;
|
|
2073
|
+
const canEdit = inlineEditEnabled && column.editable && onCellEdit;
|
|
2074
|
+
const getCellValue = () => {
|
|
2075
|
+
if (column.accessorFn) return column.accessorFn(row);
|
|
2076
|
+
if (column.accessorKey) {
|
|
2077
|
+
const keys = column.accessorKey.split(".");
|
|
2078
|
+
return keys.reduce((obj, key) => obj?.[key], row);
|
|
2079
|
+
}
|
|
2080
|
+
return void 0;
|
|
2081
|
+
};
|
|
2082
|
+
return /* @__PURE__ */ jsx(
|
|
2083
|
+
TableCell,
|
|
2084
|
+
{
|
|
2085
|
+
style: {
|
|
2086
|
+
width: getEffectiveWidth(column),
|
|
2087
|
+
minWidth: column.minWidth,
|
|
2088
|
+
maxWidth: column.maxWidth
|
|
2089
|
+
},
|
|
2090
|
+
className: cn(
|
|
2091
|
+
canEdit && !isEditing && "cursor-pointer hover:bg-muted/50"
|
|
2092
|
+
),
|
|
2093
|
+
onClick: (e) => {
|
|
2094
|
+
if (canEdit && !isEditing) {
|
|
2095
|
+
e.stopPropagation();
|
|
2096
|
+
setEditingCell({ rowId, columnId: column.id });
|
|
2097
|
+
}
|
|
2098
|
+
},
|
|
2099
|
+
children: isEditing ? /* @__PURE__ */ jsx(
|
|
2100
|
+
InlineEditCell,
|
|
2101
|
+
{
|
|
2102
|
+
value: getCellValue(),
|
|
2103
|
+
columnId: column.id,
|
|
2104
|
+
rowId,
|
|
2105
|
+
editType: column.editType,
|
|
2106
|
+
editOptions: column.editOptions,
|
|
2107
|
+
validate: column.validate ? (v) => column.validate(v, row) : void 0,
|
|
2108
|
+
onSave: async (value) => {
|
|
2109
|
+
await onCellEdit(rowId, column.id, value, row);
|
|
2110
|
+
setEditingCell(null);
|
|
2111
|
+
},
|
|
2112
|
+
onCancel: () => setEditingCell(null)
|
|
2113
|
+
}
|
|
2114
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2115
|
+
isLoading && /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin inline-block mr-2" }),
|
|
2116
|
+
renderCellContent(column, row)
|
|
2117
|
+
] })
|
|
2118
|
+
},
|
|
2119
|
+
column.id
|
|
2120
|
+
);
|
|
2121
|
+
}),
|
|
2122
|
+
rowActions && rowActions.length > 0 && /* @__PURE__ */ jsx(TableCell, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
2123
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [
|
|
2124
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Open menu" }),
|
|
2125
|
+
/* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
|
|
2126
|
+
] }) }),
|
|
2127
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
|
|
2128
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Actions" }),
|
|
2129
|
+
rowActions.map((action, idx) => {
|
|
2130
|
+
const isDisabled = action.disabled?.(row) || false;
|
|
2131
|
+
return /* @__PURE__ */ jsxs(
|
|
2132
|
+
DropdownMenuItem,
|
|
2133
|
+
{
|
|
2134
|
+
onClick: () => !isDisabled && action.onClick(row),
|
|
2135
|
+
disabled: isDisabled,
|
|
2136
|
+
className: cn(action.destructive && "text-red-600"),
|
|
2137
|
+
children: [
|
|
2138
|
+
action.icon && /* @__PURE__ */ jsx(action.icon, { className: "mr-2 h-4 w-4" }),
|
|
2139
|
+
action.label
|
|
2140
|
+
]
|
|
2141
|
+
},
|
|
2142
|
+
action.id
|
|
2143
|
+
);
|
|
2144
|
+
})
|
|
2145
|
+
] })
|
|
2146
|
+
] }) })
|
|
2147
|
+
]
|
|
2148
|
+
},
|
|
2149
|
+
rowId
|
|
2150
|
+
);
|
|
2151
|
+
}) })
|
|
2152
|
+
] }) });
|
|
2153
|
+
}
|
|
2154
|
+
function TablePagination({
|
|
2155
|
+
pagination,
|
|
2156
|
+
showSelectAllPrompt,
|
|
2157
|
+
onSelectAllPages,
|
|
2158
|
+
totalFilteredCount,
|
|
2159
|
+
className
|
|
2160
|
+
}) {
|
|
2161
|
+
const {
|
|
2162
|
+
currentPage,
|
|
2163
|
+
totalPages,
|
|
2164
|
+
totalCount,
|
|
2165
|
+
canGoNext,
|
|
2166
|
+
canGoPrevious,
|
|
2167
|
+
goToPage,
|
|
2168
|
+
goToFirstPage,
|
|
2169
|
+
goToLastPage,
|
|
2170
|
+
goToNextPage,
|
|
2171
|
+
goToPreviousPage,
|
|
2172
|
+
getPageNumbers,
|
|
2173
|
+
getDisplayRange
|
|
2174
|
+
} = pagination;
|
|
2175
|
+
const { start, end } = getDisplayRange();
|
|
2176
|
+
const pageNumbers = getPageNumbers();
|
|
2177
|
+
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
2178
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4", children: [
|
|
2179
|
+
/* @__PURE__ */ jsxs("div", { className: "text-sm text-muted-foreground", children: [
|
|
2180
|
+
"Showing ",
|
|
2181
|
+
/* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
|
|
2182
|
+
start,
|
|
2183
|
+
"-",
|
|
2184
|
+
end
|
|
2185
|
+
] }),
|
|
2186
|
+
" of",
|
|
2187
|
+
" ",
|
|
2188
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: totalCount }),
|
|
2189
|
+
totalFilteredCount && totalFilteredCount !== totalCount && /* @__PURE__ */ jsx("span", { children: " filtered items" })
|
|
2190
|
+
] }),
|
|
2191
|
+
totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2192
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground hidden sm:inline", children: [
|
|
2193
|
+
"Page ",
|
|
2194
|
+
currentPage,
|
|
2195
|
+
" of ",
|
|
2196
|
+
totalPages
|
|
2197
|
+
] }),
|
|
2198
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
2199
|
+
/* @__PURE__ */ jsx(
|
|
2200
|
+
Button,
|
|
2201
|
+
{
|
|
2202
|
+
variant: "outline",
|
|
2203
|
+
size: "sm",
|
|
2204
|
+
onClick: goToFirstPage,
|
|
2205
|
+
disabled: !canGoPrevious,
|
|
2206
|
+
className: "h-8 w-8 p-0",
|
|
2207
|
+
title: "First page",
|
|
2208
|
+
children: /* @__PURE__ */ jsx(ChevronsLeft, { className: "h-4 w-4" })
|
|
2209
|
+
}
|
|
2210
|
+
),
|
|
2211
|
+
/* @__PURE__ */ jsx(
|
|
2212
|
+
Button,
|
|
2213
|
+
{
|
|
2214
|
+
variant: "outline",
|
|
2215
|
+
size: "sm",
|
|
2216
|
+
onClick: goToPreviousPage,
|
|
2217
|
+
disabled: !canGoPrevious,
|
|
2218
|
+
className: "h-8 w-8 p-0",
|
|
2219
|
+
title: "Previous page",
|
|
2220
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
|
|
2221
|
+
}
|
|
2222
|
+
),
|
|
2223
|
+
pageNumbers.map((pageNum, idx) => {
|
|
2224
|
+
if (pageNum === "...") {
|
|
2225
|
+
return /* @__PURE__ */ jsx(
|
|
2226
|
+
"span",
|
|
2227
|
+
{
|
|
2228
|
+
className: "flex items-center px-2 text-muted-foreground",
|
|
2229
|
+
children: "\u2022\u2022\u2022"
|
|
2230
|
+
},
|
|
2231
|
+
`ellipsis-${idx}`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
const page = pageNum;
|
|
2235
|
+
return /* @__PURE__ */ jsx(
|
|
2236
|
+
Button,
|
|
2237
|
+
{
|
|
2238
|
+
variant: page === currentPage ? "default" : "outline",
|
|
2239
|
+
size: "sm",
|
|
2240
|
+
onClick: () => goToPage(page),
|
|
2241
|
+
className: "h-8 w-8 p-0",
|
|
2242
|
+
children: page
|
|
2243
|
+
},
|
|
2244
|
+
page
|
|
2245
|
+
);
|
|
2246
|
+
}),
|
|
2247
|
+
/* @__PURE__ */ jsx(
|
|
2248
|
+
Button,
|
|
2249
|
+
{
|
|
2250
|
+
variant: "outline",
|
|
2251
|
+
size: "sm",
|
|
2252
|
+
onClick: goToNextPage,
|
|
2253
|
+
disabled: !canGoNext,
|
|
2254
|
+
className: "h-8 w-8 p-0",
|
|
2255
|
+
title: "Next page",
|
|
2256
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
|
|
2257
|
+
}
|
|
2258
|
+
),
|
|
2259
|
+
/* @__PURE__ */ jsx(
|
|
2260
|
+
Button,
|
|
2261
|
+
{
|
|
2262
|
+
variant: "outline",
|
|
2263
|
+
size: "sm",
|
|
2264
|
+
onClick: goToLastPage,
|
|
2265
|
+
disabled: !canGoNext,
|
|
2266
|
+
className: "h-8 w-8 p-0",
|
|
2267
|
+
title: "Last page",
|
|
2268
|
+
children: /* @__PURE__ */ jsx(ChevronsRight, { className: "h-4 w-4" })
|
|
2269
|
+
}
|
|
2270
|
+
)
|
|
2271
|
+
] })
|
|
2272
|
+
] })
|
|
2273
|
+
] }),
|
|
2274
|
+
showSelectAllPrompt && onSelectAllPages && totalFilteredCount && totalFilteredCount > end && /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center justify-center p-3 bg-blue-50 border border-blue-200 rounded-lg", children: [
|
|
2275
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm text-blue-900 mr-2", children: [
|
|
2276
|
+
"Select all ",
|
|
2277
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: totalFilteredCount }),
|
|
2278
|
+
" items?"
|
|
2279
|
+
] }),
|
|
2280
|
+
/* @__PURE__ */ jsxs(
|
|
2281
|
+
Button,
|
|
2282
|
+
{
|
|
2283
|
+
size: "sm",
|
|
2284
|
+
variant: "link",
|
|
2285
|
+
onClick: onSelectAllPages,
|
|
2286
|
+
className: "text-blue-700 hover:text-blue-900 font-semibold p-0 h-auto",
|
|
2287
|
+
children: [
|
|
2288
|
+
"Select all ",
|
|
2289
|
+
totalFilteredCount,
|
|
2290
|
+
" items"
|
|
2291
|
+
]
|
|
2292
|
+
}
|
|
2293
|
+
)
|
|
2294
|
+
] })
|
|
2295
|
+
] });
|
|
2296
|
+
}
|
|
2297
|
+
function getColumnValue(row, column) {
|
|
2298
|
+
if (column.accessorFn) {
|
|
2299
|
+
return column.accessorFn(row);
|
|
2300
|
+
}
|
|
2301
|
+
if (column.accessorKey) {
|
|
2302
|
+
const keys = column.accessorKey.split(".");
|
|
2303
|
+
let value = row;
|
|
2304
|
+
for (const key of keys) {
|
|
2305
|
+
value = value?.[key];
|
|
2306
|
+
}
|
|
2307
|
+
return value;
|
|
2308
|
+
}
|
|
2309
|
+
return "";
|
|
2310
|
+
}
|
|
2311
|
+
function formatValueForExport(value) {
|
|
2312
|
+
if (value == null) {
|
|
2313
|
+
return "";
|
|
2314
|
+
}
|
|
2315
|
+
if (value instanceof Date) {
|
|
2316
|
+
return value.toISOString();
|
|
2317
|
+
}
|
|
2318
|
+
if (typeof value === "object") {
|
|
2319
|
+
return JSON.stringify(value);
|
|
2320
|
+
}
|
|
2321
|
+
if (typeof value === "boolean") {
|
|
2322
|
+
return value ? "Yes" : "No";
|
|
2323
|
+
}
|
|
2324
|
+
return String(value);
|
|
2325
|
+
}
|
|
2326
|
+
function getColumnHeader(column) {
|
|
2327
|
+
if (typeof column.header === "string") {
|
|
2328
|
+
return column.header;
|
|
2329
|
+
}
|
|
2330
|
+
return column.id;
|
|
2331
|
+
}
|
|
2332
|
+
function prepareExportData(data, columns, includeHeaders = true) {
|
|
2333
|
+
const rows = [];
|
|
2334
|
+
if (includeHeaders) {
|
|
2335
|
+
rows.push(columns.map(getColumnHeader));
|
|
2336
|
+
}
|
|
2337
|
+
for (const row of data) {
|
|
2338
|
+
const rowData = [];
|
|
2339
|
+
for (const column of columns) {
|
|
2340
|
+
const value = getColumnValue(row, column);
|
|
2341
|
+
rowData.push(formatValueForExport(value));
|
|
2342
|
+
}
|
|
2343
|
+
rows.push(rowData);
|
|
2344
|
+
}
|
|
2345
|
+
return rows;
|
|
2346
|
+
}
|
|
2347
|
+
function arrayToCSV(data) {
|
|
2348
|
+
return data.map(
|
|
2349
|
+
(row) => row.map((cell) => {
|
|
2350
|
+
const needsQuotes = /[,"\n\r]/.test(cell);
|
|
2351
|
+
if (needsQuotes) {
|
|
2352
|
+
return `"${cell.replace(/"/g, '""')}"`;
|
|
2353
|
+
}
|
|
2354
|
+
return cell;
|
|
2355
|
+
}).join(",")
|
|
2356
|
+
).join("\n");
|
|
2357
|
+
}
|
|
2358
|
+
function downloadFile(content, filename, mimeType) {
|
|
2359
|
+
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType });
|
|
2360
|
+
const url = URL.createObjectURL(blob);
|
|
2361
|
+
const link = document.createElement("a");
|
|
2362
|
+
link.href = url;
|
|
2363
|
+
link.download = filename;
|
|
2364
|
+
document.body.appendChild(link);
|
|
2365
|
+
link.click();
|
|
2366
|
+
document.body.removeChild(link);
|
|
2367
|
+
URL.revokeObjectURL(url);
|
|
2368
|
+
}
|
|
2369
|
+
function exportToCSV(data, columns, filename, includeHeaders = true) {
|
|
2370
|
+
if (!data || data.length === 0) {
|
|
2371
|
+
throw new Error("No data to export");
|
|
2372
|
+
}
|
|
2373
|
+
if (!columns || columns.length === 0) {
|
|
2374
|
+
throw new Error("No columns specified for export");
|
|
2375
|
+
}
|
|
2376
|
+
const exportData2 = prepareExportData(data, columns, includeHeaders);
|
|
2377
|
+
const csv = arrayToCSV(exportData2);
|
|
2378
|
+
downloadFile(csv, `${filename}.csv`, "text/csv;charset=utf-8;");
|
|
2379
|
+
}
|
|
2380
|
+
function exportToExcel(data, columns, filename, includeHeaders = true) {
|
|
2381
|
+
if (!data || data.length === 0) {
|
|
2382
|
+
throw new Error("No data to export");
|
|
2383
|
+
}
|
|
2384
|
+
if (!columns || columns.length === 0) {
|
|
2385
|
+
throw new Error("No columns specified for export");
|
|
2386
|
+
}
|
|
2387
|
+
const exportData2 = prepareExportData(data, columns, includeHeaders);
|
|
2388
|
+
const ws = XLSX.utils.aoa_to_sheet(exportData2);
|
|
2389
|
+
const wb = XLSX.utils.book_new();
|
|
2390
|
+
XLSX.utils.book_append_sheet(wb, ws, "Data");
|
|
2391
|
+
const columnWidths = columns.map((column, idx) => {
|
|
2392
|
+
const header = getColumnHeader(column);
|
|
2393
|
+
const maxWidth = exportData2.reduce((max, row) => {
|
|
2394
|
+
const cellValue = row[idx] || "";
|
|
2395
|
+
return Math.max(max, cellValue.length);
|
|
2396
|
+
}, header.length);
|
|
2397
|
+
return { wch: Math.min(maxWidth + 2, 50) };
|
|
2398
|
+
});
|
|
2399
|
+
ws["!cols"] = columnWidths;
|
|
2400
|
+
const excelBuffer = XLSX.write(wb, { bookType: "xlsx", type: "array" });
|
|
2401
|
+
const blob = new Blob([excelBuffer], {
|
|
2402
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
2403
|
+
});
|
|
2404
|
+
downloadFile(blob, `${filename}.xlsx`, blob.type);
|
|
2405
|
+
}
|
|
2406
|
+
function exportData(data, columns, options) {
|
|
2407
|
+
const { format, filename, includeHeaders = true } = options;
|
|
2408
|
+
switch (format) {
|
|
2409
|
+
case "csv":
|
|
2410
|
+
exportToCSV(data, columns, filename, includeHeaders);
|
|
2411
|
+
break;
|
|
2412
|
+
case "excel":
|
|
2413
|
+
exportToExcel(data, columns, filename, includeHeaders);
|
|
2414
|
+
break;
|
|
2415
|
+
default:
|
|
2416
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
function generateExportFilename(baseFilename) {
|
|
2420
|
+
const now = /* @__PURE__ */ new Date();
|
|
2421
|
+
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, -5);
|
|
2422
|
+
return `${baseFilename}_${timestamp}`;
|
|
2423
|
+
}
|
|
2424
|
+
function ExportButton({
|
|
2425
|
+
data,
|
|
2426
|
+
filteredData,
|
|
2427
|
+
selectedData,
|
|
2428
|
+
columns,
|
|
2429
|
+
baseFilename = "export",
|
|
2430
|
+
disabled = false,
|
|
2431
|
+
className,
|
|
2432
|
+
showProgress = true,
|
|
2433
|
+
onExportStart,
|
|
2434
|
+
onExportComplete,
|
|
2435
|
+
onExportError
|
|
2436
|
+
}) {
|
|
2437
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
2438
|
+
const [progress, setProgress] = useState(0);
|
|
2439
|
+
const hasFilteredData = filteredData && filteredData.length > 0 && filteredData.length < data.length;
|
|
2440
|
+
const hasSelectedData = selectedData && selectedData.length > 0;
|
|
2441
|
+
const handleExport = useCallback(
|
|
2442
|
+
async (format, scope) => {
|
|
2443
|
+
setIsExporting(true);
|
|
2444
|
+
setProgress(0);
|
|
2445
|
+
try {
|
|
2446
|
+
onExportStart?.(format, scope);
|
|
2447
|
+
let dataToExport;
|
|
2448
|
+
let scopeLabel;
|
|
2449
|
+
switch (scope) {
|
|
2450
|
+
case "selected":
|
|
2451
|
+
if (!selectedData || selectedData.length === 0) {
|
|
2452
|
+
throw new Error("No rows selected");
|
|
2453
|
+
}
|
|
2454
|
+
dataToExport = selectedData;
|
|
2455
|
+
scopeLabel = "selected";
|
|
2456
|
+
break;
|
|
2457
|
+
case "filtered":
|
|
2458
|
+
if (!filteredData || filteredData.length === 0) {
|
|
2459
|
+
throw new Error("No filtered data available");
|
|
2460
|
+
}
|
|
2461
|
+
dataToExport = filteredData;
|
|
2462
|
+
scopeLabel = "filtered";
|
|
2463
|
+
break;
|
|
2464
|
+
case "all":
|
|
2465
|
+
default:
|
|
2466
|
+
dataToExport = data;
|
|
2467
|
+
scopeLabel = "all";
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
if (showProgress && dataToExport.length > 100) {
|
|
2471
|
+
setProgress(25);
|
|
2472
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2473
|
+
setProgress(50);
|
|
2474
|
+
}
|
|
2475
|
+
const filename = `${baseFilename}_${scopeLabel}_${generateExportFilename("")}`.replace(
|
|
2476
|
+
/__/g,
|
|
2477
|
+
"_"
|
|
2478
|
+
);
|
|
2479
|
+
if (format === "csv") {
|
|
2480
|
+
exportToCSV(dataToExport, columns, filename);
|
|
2481
|
+
} else {
|
|
2482
|
+
exportToExcel(dataToExport, columns, filename);
|
|
2483
|
+
}
|
|
2484
|
+
if (showProgress) {
|
|
2485
|
+
setProgress(100);
|
|
2486
|
+
}
|
|
2487
|
+
onExportComplete?.(format, scope, dataToExport.length);
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
console.error("Export failed:", error);
|
|
2490
|
+
const errorObj = error instanceof Error ? error : new Error("Export failed");
|
|
2491
|
+
onExportError?.(errorObj);
|
|
2492
|
+
} finally {
|
|
2493
|
+
if (showProgress) {
|
|
2494
|
+
setTimeout(() => {
|
|
2495
|
+
setIsExporting(false);
|
|
2496
|
+
setProgress(0);
|
|
2497
|
+
}, 500);
|
|
2498
|
+
} else {
|
|
2499
|
+
setIsExporting(false);
|
|
2500
|
+
setProgress(0);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
},
|
|
2504
|
+
[
|
|
2505
|
+
data,
|
|
2506
|
+
filteredData,
|
|
2507
|
+
selectedData,
|
|
2508
|
+
columns,
|
|
2509
|
+
baseFilename,
|
|
2510
|
+
showProgress,
|
|
2511
|
+
onExportStart,
|
|
2512
|
+
onExportComplete,
|
|
2513
|
+
onExportError
|
|
2514
|
+
]
|
|
2515
|
+
);
|
|
2516
|
+
const isDisabled = disabled || isExporting || data.length === 0;
|
|
2517
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
2518
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
2519
|
+
Button,
|
|
2520
|
+
{
|
|
2521
|
+
variant: "outline",
|
|
2522
|
+
size: "sm",
|
|
2523
|
+
disabled: isDisabled,
|
|
2524
|
+
className,
|
|
2525
|
+
"aria-label": "Export data",
|
|
2526
|
+
children: isExporting ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2527
|
+
/* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
|
|
2528
|
+
"Exporting",
|
|
2529
|
+
showProgress && progress > 0 ? ` ${progress}%` : "..."
|
|
2530
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2531
|
+
/* @__PURE__ */ jsx(Download, { className: "mr-2 h-4 w-4" }),
|
|
2532
|
+
"Export"
|
|
2533
|
+
] })
|
|
2534
|
+
}
|
|
2535
|
+
) }),
|
|
2536
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [
|
|
2537
|
+
/* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs font-semibold text-muted-foreground", children: "CSV Format" }),
|
|
2538
|
+
/* @__PURE__ */ jsxs(
|
|
2539
|
+
DropdownMenuItem,
|
|
2540
|
+
{
|
|
2541
|
+
onClick: () => handleExport("csv", "all"),
|
|
2542
|
+
disabled: isExporting || data.length === 0,
|
|
2543
|
+
children: [
|
|
2544
|
+
/* @__PURE__ */ jsx(FileText, { className: "mr-2 h-4 w-4" }),
|
|
2545
|
+
"Export All (",
|
|
2546
|
+
data.length,
|
|
2547
|
+
" rows)"
|
|
2548
|
+
]
|
|
2549
|
+
}
|
|
2550
|
+
),
|
|
2551
|
+
hasFilteredData && /* @__PURE__ */ jsxs(
|
|
2552
|
+
DropdownMenuItem,
|
|
2553
|
+
{
|
|
2554
|
+
onClick: () => handleExport("csv", "filtered"),
|
|
2555
|
+
disabled: isExporting,
|
|
2556
|
+
children: [
|
|
2557
|
+
/* @__PURE__ */ jsx(FileText, { className: "mr-2 h-4 w-4" }),
|
|
2558
|
+
"Export Filtered (",
|
|
2559
|
+
filteredData?.length || 0,
|
|
2560
|
+
" rows)"
|
|
2561
|
+
]
|
|
2562
|
+
}
|
|
2563
|
+
),
|
|
2564
|
+
hasSelectedData && /* @__PURE__ */ jsxs(
|
|
2565
|
+
DropdownMenuItem,
|
|
2566
|
+
{
|
|
2567
|
+
onClick: () => handleExport("csv", "selected"),
|
|
2568
|
+
disabled: isExporting,
|
|
2569
|
+
children: [
|
|
2570
|
+
/* @__PURE__ */ jsx(FileText, { className: "mr-2 h-4 w-4" }),
|
|
2571
|
+
"Export Selected (",
|
|
2572
|
+
selectedData?.length || 0,
|
|
2573
|
+
" rows)"
|
|
2574
|
+
]
|
|
2575
|
+
}
|
|
2576
|
+
),
|
|
2577
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
2578
|
+
/* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs font-semibold text-muted-foreground", children: "Excel Format" }),
|
|
2579
|
+
/* @__PURE__ */ jsxs(
|
|
2580
|
+
DropdownMenuItem,
|
|
2581
|
+
{
|
|
2582
|
+
onClick: () => handleExport("excel", "all"),
|
|
2583
|
+
disabled: isExporting || data.length === 0,
|
|
2584
|
+
children: [
|
|
2585
|
+
/* @__PURE__ */ jsx(FileSpreadsheet, { className: "mr-2 h-4 w-4" }),
|
|
2586
|
+
"Export All (",
|
|
2587
|
+
data.length,
|
|
2588
|
+
" rows)"
|
|
2589
|
+
]
|
|
2590
|
+
}
|
|
2591
|
+
),
|
|
2592
|
+
hasFilteredData && /* @__PURE__ */ jsxs(
|
|
2593
|
+
DropdownMenuItem,
|
|
2594
|
+
{
|
|
2595
|
+
onClick: () => handleExport("excel", "filtered"),
|
|
2596
|
+
disabled: isExporting,
|
|
2597
|
+
children: [
|
|
2598
|
+
/* @__PURE__ */ jsx(FileSpreadsheet, { className: "mr-2 h-4 w-4" }),
|
|
2599
|
+
"Export Filtered (",
|
|
2600
|
+
filteredData?.length || 0,
|
|
2601
|
+
" rows)"
|
|
2602
|
+
]
|
|
2603
|
+
}
|
|
2604
|
+
),
|
|
2605
|
+
hasSelectedData && /* @__PURE__ */ jsxs(
|
|
2606
|
+
DropdownMenuItem,
|
|
2607
|
+
{
|
|
2608
|
+
onClick: () => handleExport("excel", "selected"),
|
|
2609
|
+
disabled: isExporting,
|
|
2610
|
+
children: [
|
|
2611
|
+
/* @__PURE__ */ jsx(FileSpreadsheet, { className: "mr-2 h-4 w-4" }),
|
|
2612
|
+
"Export Selected (",
|
|
2613
|
+
selectedData?.length || 0,
|
|
2614
|
+
" rows)"
|
|
2615
|
+
]
|
|
2616
|
+
}
|
|
2617
|
+
)
|
|
2618
|
+
] })
|
|
2619
|
+
] });
|
|
2620
|
+
}
|
|
2621
|
+
var Dialog = DialogPrimitive.Root;
|
|
2622
|
+
var DialogTrigger = DialogPrimitive.Trigger;
|
|
2623
|
+
var DialogPortal = DialogPrimitive.Portal;
|
|
2624
|
+
var DialogClose = DialogPrimitive.Close;
|
|
2625
|
+
var DialogOverlay = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2626
|
+
DialogPrimitive.Overlay,
|
|
2627
|
+
{
|
|
2628
|
+
ref,
|
|
2629
|
+
className: cn(
|
|
2630
|
+
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
2631
|
+
className
|
|
2632
|
+
),
|
|
2633
|
+
...props
|
|
2634
|
+
}
|
|
2635
|
+
));
|
|
2636
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
2637
|
+
var DialogContent = React5.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(DialogPortal, { children: [
|
|
2638
|
+
/* @__PURE__ */ jsx(DialogOverlay, {}),
|
|
2639
|
+
/* @__PURE__ */ jsxs(
|
|
2640
|
+
DialogPrimitive.Content,
|
|
2641
|
+
{
|
|
2642
|
+
ref,
|
|
2643
|
+
className: cn(
|
|
2644
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-4 sm:p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[95vh] max-sm:max-h-[90vh] max-sm:m-4 max-sm:w-[calc(100%-2rem)] overflow-hidden",
|
|
2645
|
+
className
|
|
2646
|
+
),
|
|
2647
|
+
...props,
|
|
2648
|
+
children: [
|
|
2649
|
+
children,
|
|
2650
|
+
/* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "absolute right-3 top-3 sm:right-4 sm:top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground min-h-[44px] min-w-[44px] flex items-center justify-center", children: [
|
|
2651
|
+
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
2652
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
|
|
2653
|
+
] })
|
|
2654
|
+
]
|
|
2655
|
+
}
|
|
2656
|
+
)
|
|
2657
|
+
] }));
|
|
2658
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
2659
|
+
var DialogHeader = ({
|
|
2660
|
+
className,
|
|
2661
|
+
...props
|
|
2662
|
+
}) => /* @__PURE__ */ jsx(
|
|
2663
|
+
"div",
|
|
2664
|
+
{
|
|
2665
|
+
className: cn(
|
|
2666
|
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
2667
|
+
className
|
|
2668
|
+
),
|
|
2669
|
+
...props
|
|
2670
|
+
}
|
|
2671
|
+
);
|
|
2672
|
+
DialogHeader.displayName = "DialogHeader";
|
|
2673
|
+
var DialogFooter = ({
|
|
2674
|
+
className,
|
|
2675
|
+
...props
|
|
2676
|
+
}) => /* @__PURE__ */ jsx(
|
|
2677
|
+
"div",
|
|
2678
|
+
{
|
|
2679
|
+
className: cn(
|
|
2680
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 space-y-2 space-y-reverse sm:space-y-0",
|
|
2681
|
+
className
|
|
2682
|
+
),
|
|
2683
|
+
...props
|
|
2684
|
+
}
|
|
2685
|
+
);
|
|
2686
|
+
DialogFooter.displayName = "DialogFooter";
|
|
2687
|
+
var DialogTitle = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2688
|
+
DialogPrimitive.Title,
|
|
2689
|
+
{
|
|
2690
|
+
ref,
|
|
2691
|
+
className: cn(
|
|
2692
|
+
"text-lg font-semibold leading-none tracking-tight",
|
|
2693
|
+
className
|
|
2694
|
+
),
|
|
2695
|
+
...props
|
|
2696
|
+
}
|
|
2697
|
+
));
|
|
2698
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
2699
|
+
var DialogDescription = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2700
|
+
DialogPrimitive.Description,
|
|
2701
|
+
{
|
|
2702
|
+
ref,
|
|
2703
|
+
className: cn("text-sm text-muted-foreground", className),
|
|
2704
|
+
...props
|
|
2705
|
+
}
|
|
2706
|
+
));
|
|
2707
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
2708
|
+
function SavedViewsDropdown({
|
|
2709
|
+
views,
|
|
2710
|
+
currentViewId,
|
|
2711
|
+
onSaveView,
|
|
2712
|
+
onUpdateView,
|
|
2713
|
+
onDeleteView,
|
|
2714
|
+
onLoadView,
|
|
2715
|
+
getCurrentViewState
|
|
2716
|
+
}) {
|
|
2717
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
2718
|
+
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
|
|
2719
|
+
const [viewName, setViewName] = useState("");
|
|
2720
|
+
const [isDefault, setIsDefault] = useState(false);
|
|
2721
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
2722
|
+
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
|
|
2723
|
+
const currentView = views.find((v) => v.id === currentViewId);
|
|
2724
|
+
const handleSaveView = async () => {
|
|
2725
|
+
if (!viewName.trim()) return;
|
|
2726
|
+
setIsSaving(true);
|
|
2727
|
+
try {
|
|
2728
|
+
const viewState = getCurrentViewState();
|
|
2729
|
+
await onSaveView({
|
|
2730
|
+
name: viewName.trim(),
|
|
2731
|
+
isDefault,
|
|
2732
|
+
...viewState
|
|
2733
|
+
});
|
|
2734
|
+
setIsSaveDialogOpen(false);
|
|
2735
|
+
setViewName("");
|
|
2736
|
+
setIsDefault(false);
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
console.error("Failed to save view:", error);
|
|
2739
|
+
} finally {
|
|
2740
|
+
setIsSaving(false);
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
const handleDeleteView = async (viewId) => {
|
|
2744
|
+
if (!onDeleteView) return;
|
|
2745
|
+
try {
|
|
2746
|
+
await onDeleteView(viewId);
|
|
2747
|
+
setDeleteConfirmId(null);
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
console.error("Failed to delete view:", error);
|
|
2750
|
+
}
|
|
2751
|
+
};
|
|
2752
|
+
const handleSetDefault = async (viewId) => {
|
|
2753
|
+
if (!onUpdateView) return;
|
|
2754
|
+
try {
|
|
2755
|
+
for (const view of views) {
|
|
2756
|
+
if (view.isDefault && view.id !== viewId) {
|
|
2757
|
+
await onUpdateView(view.id, { isDefault: false });
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
await onUpdateView(viewId, { isDefault: true });
|
|
2761
|
+
} catch (error) {
|
|
2762
|
+
console.error("Failed to set default view:", error);
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2766
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { open: isOpen, onOpenChange: setIsOpen, children: [
|
|
2767
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
|
|
2768
|
+
/* @__PURE__ */ jsx(BookmarkPlus, { className: "mr-2 h-4 w-4" }),
|
|
2769
|
+
currentView ? currentView.name : "Views",
|
|
2770
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4" })
|
|
2771
|
+
] }) }),
|
|
2772
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [
|
|
2773
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Saved Views" }),
|
|
2774
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
2775
|
+
views.length === 0 ? /* @__PURE__ */ jsx(DropdownMenuItem, { disabled: true, children: "No saved views" }) : views.map((view) => /* @__PURE__ */ jsxs(
|
|
2776
|
+
DropdownMenuItem,
|
|
2777
|
+
{
|
|
2778
|
+
className: "flex items-center justify-between cursor-pointer",
|
|
2779
|
+
onSelect: (e) => {
|
|
2780
|
+
e.preventDefault();
|
|
2781
|
+
if (deleteConfirmId === view.id) return;
|
|
2782
|
+
onLoadView(view.id);
|
|
2783
|
+
setIsOpen(false);
|
|
2784
|
+
},
|
|
2785
|
+
children: [
|
|
2786
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2787
|
+
currentViewId === view.id && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-primary" }),
|
|
2788
|
+
currentViewId !== view.id && /* @__PURE__ */ jsx("div", { className: "w-4" }),
|
|
2789
|
+
/* @__PURE__ */ jsx("span", { children: view.name }),
|
|
2790
|
+
view.isDefault && /* @__PURE__ */ jsx(Star, { className: "h-3 w-3 text-yellow-500 fill-yellow-500" })
|
|
2791
|
+
] }),
|
|
2792
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
2793
|
+
onUpdateView && !view.isDefault && /* @__PURE__ */ jsx(
|
|
2794
|
+
"button",
|
|
2795
|
+
{
|
|
2796
|
+
onClick: (e) => {
|
|
2797
|
+
e.stopPropagation();
|
|
2798
|
+
handleSetDefault(view.id);
|
|
2799
|
+
},
|
|
2800
|
+
className: "p-1 hover:bg-muted rounded opacity-50 hover:opacity-100",
|
|
2801
|
+
title: "Set as default",
|
|
2802
|
+
children: /* @__PURE__ */ jsx(Star, { className: "h-3 w-3" })
|
|
2803
|
+
}
|
|
2804
|
+
),
|
|
2805
|
+
onDeleteView && /* @__PURE__ */ jsx(
|
|
2806
|
+
"button",
|
|
2807
|
+
{
|
|
2808
|
+
onClick: (e) => {
|
|
2809
|
+
e.stopPropagation();
|
|
2810
|
+
if (deleteConfirmId === view.id) {
|
|
2811
|
+
handleDeleteView(view.id);
|
|
2812
|
+
} else {
|
|
2813
|
+
setDeleteConfirmId(view.id);
|
|
2814
|
+
setTimeout(() => setDeleteConfirmId(null), 3e3);
|
|
2815
|
+
}
|
|
2816
|
+
},
|
|
2817
|
+
className: cn(
|
|
2818
|
+
"p-1 hover:bg-muted rounded",
|
|
2819
|
+
deleteConfirmId === view.id ? "text-red-600 hover:text-red-700" : "opacity-50 hover:opacity-100"
|
|
2820
|
+
),
|
|
2821
|
+
title: deleteConfirmId === view.id ? "Click again to confirm" : "Delete view",
|
|
2822
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3" })
|
|
2823
|
+
}
|
|
2824
|
+
)
|
|
2825
|
+
] })
|
|
2826
|
+
]
|
|
2827
|
+
},
|
|
2828
|
+
view.id
|
|
2829
|
+
)),
|
|
2830
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
2831
|
+
/* @__PURE__ */ jsxs(
|
|
2832
|
+
DropdownMenuItem,
|
|
2833
|
+
{
|
|
2834
|
+
onSelect: (e) => {
|
|
2835
|
+
e.preventDefault();
|
|
2836
|
+
setIsSaveDialogOpen(true);
|
|
2837
|
+
setIsOpen(false);
|
|
2838
|
+
},
|
|
2839
|
+
children: [
|
|
2840
|
+
/* @__PURE__ */ jsx(BookmarkPlus, { className: "mr-2 h-4 w-4" }),
|
|
2841
|
+
"Save Current View"
|
|
2842
|
+
]
|
|
2843
|
+
}
|
|
2844
|
+
)
|
|
2845
|
+
] })
|
|
2846
|
+
] }),
|
|
2847
|
+
/* @__PURE__ */ jsx(Dialog, { open: isSaveDialogOpen, onOpenChange: setIsSaveDialogOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[425px]", children: [
|
|
2848
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
2849
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: "Save View" }),
|
|
2850
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: "Save your current table configuration as a named view." })
|
|
2851
|
+
] }),
|
|
2852
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 py-4", children: [
|
|
2853
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
2854
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "view-name", children: "View Name" }),
|
|
2855
|
+
/* @__PURE__ */ jsx(
|
|
2856
|
+
Input,
|
|
2857
|
+
{
|
|
2858
|
+
id: "view-name",
|
|
2859
|
+
value: viewName,
|
|
2860
|
+
onChange: (e) => setViewName(e.target.value),
|
|
2861
|
+
placeholder: "My Custom View",
|
|
2862
|
+
onKeyDown: (e) => {
|
|
2863
|
+
if (e.key === "Enter") handleSaveView();
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
)
|
|
2867
|
+
] }),
|
|
2868
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2869
|
+
/* @__PURE__ */ jsx(
|
|
2870
|
+
Checkbox,
|
|
2871
|
+
{
|
|
2872
|
+
id: "is-default",
|
|
2873
|
+
checked: isDefault,
|
|
2874
|
+
onCheckedChange: (checked) => setIsDefault(checked === true)
|
|
2875
|
+
}
|
|
2876
|
+
),
|
|
2877
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "is-default", className: "cursor-pointer", children: "Set as default view" })
|
|
2878
|
+
] })
|
|
2879
|
+
] }),
|
|
2880
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
2881
|
+
/* @__PURE__ */ jsx(
|
|
2882
|
+
Button,
|
|
2883
|
+
{
|
|
2884
|
+
variant: "outline",
|
|
2885
|
+
onClick: () => setIsSaveDialogOpen(false),
|
|
2886
|
+
disabled: isSaving,
|
|
2887
|
+
children: "Cancel"
|
|
2888
|
+
}
|
|
2889
|
+
),
|
|
2890
|
+
/* @__PURE__ */ jsx(
|
|
2891
|
+
Button,
|
|
2892
|
+
{
|
|
2893
|
+
onClick: handleSaveView,
|
|
2894
|
+
disabled: !viewName.trim() || isSaving,
|
|
2895
|
+
children: isSaving ? "Saving..." : "Save View"
|
|
2896
|
+
}
|
|
2897
|
+
)
|
|
2898
|
+
] })
|
|
2899
|
+
] }) })
|
|
2900
|
+
] });
|
|
2901
|
+
}
|
|
2902
|
+
function StandardTableToolbar({
|
|
2903
|
+
searchEnabled,
|
|
2904
|
+
searchPlaceholder = "Search...",
|
|
2905
|
+
searchValue = "",
|
|
2906
|
+
onSearchChange,
|
|
2907
|
+
searchInputRef,
|
|
2908
|
+
searchAutoFocus,
|
|
2909
|
+
columnVisibilityEnabled,
|
|
2910
|
+
hideableColumns = [],
|
|
2911
|
+
columnVisibility = {},
|
|
2912
|
+
onToggleColumnVisibility,
|
|
2913
|
+
exportEnabled,
|
|
2914
|
+
exportProps,
|
|
2915
|
+
exportData: exportData2 = [],
|
|
2916
|
+
exportColumns = [],
|
|
2917
|
+
savedViewsEnabled,
|
|
2918
|
+
savedViews = [],
|
|
2919
|
+
currentViewId,
|
|
2920
|
+
savedViewsProps,
|
|
2921
|
+
className
|
|
2922
|
+
}) {
|
|
2923
|
+
const hasRightSideButtons = exportEnabled || columnVisibilityEnabled || savedViewsEnabled;
|
|
2924
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-between gap-4", className), children: [
|
|
2925
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-3 flex-1", children: searchEnabled ? /* @__PURE__ */ jsx(
|
|
2926
|
+
Input,
|
|
2927
|
+
{
|
|
2928
|
+
type: "text",
|
|
2929
|
+
placeholder: searchPlaceholder,
|
|
2930
|
+
value: searchValue,
|
|
2931
|
+
onChange: (e) => onSearchChange?.(e.target.value),
|
|
2932
|
+
ref: searchInputRef,
|
|
2933
|
+
autoFocus: searchAutoFocus,
|
|
2934
|
+
className: "max-w-xl w-full"
|
|
2935
|
+
}
|
|
2936
|
+
) : /* @__PURE__ */ jsx("div", {}) }),
|
|
2937
|
+
hasRightSideButtons && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2938
|
+
exportEnabled && exportProps && /* @__PURE__ */ jsx(
|
|
2939
|
+
ExportButton,
|
|
2940
|
+
{
|
|
2941
|
+
data: exportData2,
|
|
2942
|
+
columns: exportColumns,
|
|
2943
|
+
...exportProps
|
|
2944
|
+
}
|
|
2945
|
+
),
|
|
2946
|
+
columnVisibilityEnabled && hideableColumns.length > 0 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
2947
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
|
|
2948
|
+
/* @__PURE__ */ jsx(Columns3, { className: "mr-2 h-4 w-4" }),
|
|
2949
|
+
"Columns",
|
|
2950
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4" })
|
|
2951
|
+
] }) }),
|
|
2952
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-48", children: hideableColumns.map((column) => /* @__PURE__ */ jsx(
|
|
2953
|
+
DropdownMenuCheckboxItem,
|
|
2954
|
+
{
|
|
2955
|
+
checked: columnVisibility[column.id] !== false,
|
|
2956
|
+
onCheckedChange: () => onToggleColumnVisibility?.(column.id),
|
|
2957
|
+
children: typeof column.header === "string" ? column.header : column.id
|
|
2958
|
+
},
|
|
2959
|
+
column.id
|
|
2960
|
+
)) })
|
|
2961
|
+
] }),
|
|
2962
|
+
savedViewsEnabled && savedViewsProps && /* @__PURE__ */ jsx(
|
|
2963
|
+
SavedViewsDropdown,
|
|
2964
|
+
{
|
|
2965
|
+
views: savedViews,
|
|
2966
|
+
currentViewId,
|
|
2967
|
+
...savedViewsProps
|
|
2968
|
+
}
|
|
2969
|
+
)
|
|
2970
|
+
] })
|
|
2971
|
+
] });
|
|
2972
|
+
}
|
|
2973
|
+
function UnifiedTable({
|
|
2974
|
+
data,
|
|
2975
|
+
columns,
|
|
2976
|
+
tableId,
|
|
2977
|
+
getRowId,
|
|
2978
|
+
pagination: paginationConfig,
|
|
2979
|
+
selection: selectionConfig,
|
|
2980
|
+
filters: filtersConfig,
|
|
2981
|
+
bulkActions = [],
|
|
2982
|
+
rowActions = [],
|
|
2983
|
+
search: searchConfig,
|
|
2984
|
+
sorting: sortingConfig,
|
|
2985
|
+
columnVisibility: columnVisibilityConfig,
|
|
2986
|
+
columnReorder: columnReorderConfig,
|
|
2987
|
+
columnResize: columnResizeConfig,
|
|
2988
|
+
inlineEdit: inlineEditConfig,
|
|
2989
|
+
savedViews: savedViewsConfig,
|
|
2990
|
+
urlPersistence: urlPersistenceConfig,
|
|
2991
|
+
export: exportConfig,
|
|
2992
|
+
onRowClick,
|
|
2993
|
+
mobileConfig,
|
|
2994
|
+
loading = false,
|
|
2995
|
+
loadingRows = /* @__PURE__ */ new Set(),
|
|
2996
|
+
className,
|
|
2997
|
+
emptyState,
|
|
2998
|
+
errorState
|
|
2999
|
+
}) {
|
|
3000
|
+
const searchInputRef = useRef(null);
|
|
3001
|
+
const shouldRestoreFocusRef = useRef(false);
|
|
3002
|
+
const tableRef = useRef(null);
|
|
3003
|
+
const urlState = useTableURL({
|
|
3004
|
+
tableId,
|
|
3005
|
+
persistToUrl: urlPersistenceConfig?.enabled ?? false,
|
|
3006
|
+
debounceMs: urlPersistenceConfig?.debounceMs ?? 300
|
|
3007
|
+
});
|
|
3008
|
+
const initialURLState = useMemo(() => {
|
|
3009
|
+
if (!urlPersistenceConfig?.enabled) return null;
|
|
3010
|
+
return urlState.getURLState();
|
|
3011
|
+
}, []);
|
|
3012
|
+
const [columnVisibility, setColumnVisibility] = useState(() => {
|
|
3013
|
+
if (columnVisibilityConfig?.persistKey && typeof window !== "undefined") {
|
|
3014
|
+
try {
|
|
3015
|
+
const stored = localStorage.getItem(`table-columns-${columnVisibilityConfig.persistKey}`);
|
|
3016
|
+
if (stored) {
|
|
3017
|
+
return JSON.parse(stored);
|
|
3018
|
+
}
|
|
3019
|
+
} catch {
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
const initial = {};
|
|
3023
|
+
columns.forEach((col) => {
|
|
3024
|
+
if (columnVisibilityConfig?.defaultVisible) {
|
|
3025
|
+
initial[col.id] = columnVisibilityConfig.defaultVisible.includes(col.id);
|
|
3026
|
+
} else {
|
|
3027
|
+
initial[col.id] = true;
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
return initial;
|
|
3031
|
+
});
|
|
3032
|
+
const [localSort, setLocalSort] = useState(() => {
|
|
3033
|
+
if (initialURLState) {
|
|
3034
|
+
return {
|
|
3035
|
+
sortBy: initialURLState.sortBy,
|
|
3036
|
+
sortDirection: initialURLState.sortDirection
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
return { sortBy: null, sortDirection: "asc" };
|
|
3040
|
+
});
|
|
3041
|
+
useEffect(() => {
|
|
3042
|
+
if (columnVisibilityConfig?.persistKey && typeof window !== "undefined") {
|
|
3043
|
+
try {
|
|
3044
|
+
localStorage.setItem(
|
|
3045
|
+
`table-columns-${columnVisibilityConfig.persistKey}`,
|
|
3046
|
+
JSON.stringify(columnVisibility)
|
|
3047
|
+
);
|
|
3048
|
+
} catch {
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
}, [columnVisibility, columnVisibilityConfig?.persistKey]);
|
|
3052
|
+
const toggleColumnVisibility = useCallback((columnId) => {
|
|
3053
|
+
if (columnVisibilityConfig?.alwaysVisible?.includes(columnId)) {
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
setColumnVisibility((prev) => ({
|
|
3057
|
+
...prev,
|
|
3058
|
+
[columnId]: !prev[columnId]
|
|
3059
|
+
}));
|
|
3060
|
+
}, [columnVisibilityConfig?.alwaysVisible]);
|
|
3061
|
+
const visibleColumns = useMemo(() => {
|
|
3062
|
+
if (!columnVisibilityConfig?.enabled) {
|
|
3063
|
+
return columns;
|
|
3064
|
+
}
|
|
3065
|
+
return columns.filter((col) => columnVisibility[col.id] !== false);
|
|
3066
|
+
}, [columns, columnVisibility, columnVisibilityConfig?.enabled]);
|
|
3067
|
+
const hideableColumns = useMemo(() => {
|
|
3068
|
+
return columns.filter((col) => {
|
|
3069
|
+
if (col.hideable === false) return false;
|
|
3070
|
+
if (columnVisibilityConfig?.alwaysVisible?.includes(col.id)) return false;
|
|
3071
|
+
return true;
|
|
3072
|
+
});
|
|
3073
|
+
}, [columns, columnVisibilityConfig?.alwaysVisible]);
|
|
3074
|
+
const columnReorder = useColumnReorder({
|
|
3075
|
+
columns: visibleColumns,
|
|
3076
|
+
initialOrder: columnReorderConfig?.initialOrder,
|
|
3077
|
+
enabled: columnReorderConfig?.enabled ?? false,
|
|
3078
|
+
onOrderChange: columnReorderConfig?.onOrderChange
|
|
3079
|
+
});
|
|
3080
|
+
const displayColumns = columnReorderConfig?.enabled ? columnReorder.orderedColumns : visibleColumns;
|
|
3081
|
+
const columnResize = useColumnResize({
|
|
3082
|
+
enabled: columnResizeConfig?.enabled ?? false,
|
|
3083
|
+
initialWidths: columnResizeConfig?.initialWidths,
|
|
3084
|
+
minWidth: columnResizeConfig?.minWidth ?? 50,
|
|
3085
|
+
onWidthChange: columnResizeConfig?.onWidthChange
|
|
3086
|
+
});
|
|
3087
|
+
const sortingEnabled = sortingConfig?.enabled ?? true;
|
|
3088
|
+
const currentSortBy = sortingConfig?.value?.sortBy ?? localSort.sortBy;
|
|
3089
|
+
const currentSortDirection = sortingConfig?.value?.sortDirection ?? localSort.sortDirection;
|
|
3090
|
+
const handleSort = useCallback((columnId) => {
|
|
3091
|
+
const newDirection = currentSortBy === columnId && currentSortDirection === "asc" ? "desc" : "asc";
|
|
3092
|
+
const newSort = { sortBy: columnId, sortDirection: newDirection };
|
|
3093
|
+
if (sortingConfig?.onChange) {
|
|
3094
|
+
sortingConfig.onChange(newSort);
|
|
3095
|
+
} else {
|
|
3096
|
+
setLocalSort(newSort);
|
|
3097
|
+
}
|
|
3098
|
+
urlState.setSortToURL(newSort);
|
|
3099
|
+
}, [currentSortBy, currentSortDirection, sortingConfig, urlState]);
|
|
3100
|
+
const safeData = useMemo(() => Array.isArray(data) ? data : [], [data]);
|
|
3101
|
+
const sortedData = useMemo(() => {
|
|
3102
|
+
if (sortingConfig?.serverSide || !currentSortBy) {
|
|
3103
|
+
return safeData;
|
|
3104
|
+
}
|
|
3105
|
+
const column = columns.find((c) => c.id === currentSortBy);
|
|
3106
|
+
if (!column) return safeData;
|
|
3107
|
+
return [...safeData].sort((a, b) => {
|
|
3108
|
+
let valueA;
|
|
3109
|
+
let valueB;
|
|
3110
|
+
if (column.sortingFn) {
|
|
3111
|
+
return currentSortDirection === "asc" ? column.sortingFn(a, b) : column.sortingFn(b, a);
|
|
3112
|
+
}
|
|
3113
|
+
if (column.accessorFn) {
|
|
3114
|
+
valueA = column.accessorFn(a);
|
|
3115
|
+
valueB = column.accessorFn(b);
|
|
3116
|
+
} else if (column.accessorKey) {
|
|
3117
|
+
const keys = column.accessorKey.split(".");
|
|
3118
|
+
valueA = keys.reduce((obj, key) => obj?.[key], a);
|
|
3119
|
+
valueB = keys.reduce((obj, key) => obj?.[key], b);
|
|
3120
|
+
} else {
|
|
3121
|
+
return 0;
|
|
3122
|
+
}
|
|
3123
|
+
if (valueA == null && valueB == null) return 0;
|
|
3124
|
+
if (valueA == null) return currentSortDirection === "asc" ? 1 : -1;
|
|
3125
|
+
if (valueB == null) return currentSortDirection === "asc" ? -1 : 1;
|
|
3126
|
+
if (typeof valueA === "string" && typeof valueB === "string") {
|
|
3127
|
+
const comparison2 = valueA.localeCompare(valueB, void 0, { sensitivity: "base" });
|
|
3128
|
+
return currentSortDirection === "asc" ? comparison2 : -comparison2;
|
|
3129
|
+
}
|
|
3130
|
+
if (typeof valueA === "number" && typeof valueB === "number") {
|
|
3131
|
+
return currentSortDirection === "asc" ? valueA - valueB : valueB - valueA;
|
|
3132
|
+
}
|
|
3133
|
+
const comparison = String(valueA).localeCompare(String(valueB));
|
|
3134
|
+
return currentSortDirection === "asc" ? comparison : -comparison;
|
|
3135
|
+
});
|
|
3136
|
+
}, [safeData, columns, currentSortBy, currentSortDirection, sortingConfig?.serverSide]);
|
|
3137
|
+
const pagination = usePagination({
|
|
3138
|
+
totalCount: paginationConfig?.totalCount || sortedData.length,
|
|
3139
|
+
initialPageSize: paginationConfig?.pageSize || 25,
|
|
3140
|
+
initialPage: paginationConfig?.currentPage || (initialURLState?.page ?? 1),
|
|
3141
|
+
serverSide: paginationConfig?.serverSide || false,
|
|
3142
|
+
onPageChange: paginationConfig?.onPageChange
|
|
3143
|
+
});
|
|
3144
|
+
const selection = useSelection({
|
|
3145
|
+
currentPageData: sortedData,
|
|
3146
|
+
totalCount: paginationConfig?.totalCount || sortedData.length,
|
|
3147
|
+
getRowId,
|
|
3148
|
+
onSelectionChange: selectionConfig?.onSelectionChange,
|
|
3149
|
+
onSelectAllPages: selectionConfig?.onSelectAllPages,
|
|
3150
|
+
// External controlled state
|
|
3151
|
+
externalSelectedIds: selectionConfig?.selectedIds,
|
|
3152
|
+
externalSelectAllPages: selectionConfig?.selectAllPages
|
|
3153
|
+
});
|
|
3154
|
+
const filters = useFilters({
|
|
3155
|
+
initialFilters: filtersConfig?.value || (initialURLState?.filters ?? {}),
|
|
3156
|
+
onChange: filtersConfig?.onChange
|
|
3157
|
+
});
|
|
3158
|
+
const getCurrentViewState = useCallback(() => {
|
|
3159
|
+
return {
|
|
3160
|
+
columnVisibility: columnVisibilityConfig?.enabled ? columnVisibility : void 0,
|
|
3161
|
+
columnOrder: columnReorderConfig?.enabled ? columnReorder.columnOrder : void 0,
|
|
3162
|
+
sortBy: currentSortBy,
|
|
3163
|
+
sortDirection: currentSortDirection,
|
|
3164
|
+
filters: filtersConfig?.enabled ? filters.filters : void 0,
|
|
3165
|
+
pageSize: paginationConfig?.pageSize
|
|
3166
|
+
};
|
|
3167
|
+
}, [
|
|
3168
|
+
columnVisibility,
|
|
3169
|
+
columnVisibilityConfig?.enabled,
|
|
3170
|
+
columnReorder.columnOrder,
|
|
3171
|
+
columnReorderConfig?.enabled,
|
|
3172
|
+
currentSortBy,
|
|
3173
|
+
currentSortDirection,
|
|
3174
|
+
filters.filters,
|
|
3175
|
+
filtersConfig?.enabled,
|
|
3176
|
+
paginationConfig?.pageSize
|
|
3177
|
+
]);
|
|
3178
|
+
useTableKeyboard({
|
|
3179
|
+
data: sortedData,
|
|
3180
|
+
getRowId,
|
|
3181
|
+
selectedIds: selection.selectedIds,
|
|
3182
|
+
onToggleRow: selection.toggleRow,
|
|
3183
|
+
onToggleAll: selection.toggleAll,
|
|
3184
|
+
onRowClick,
|
|
3185
|
+
onDelete: bulkActions.find((action) => action.id === "delete")?.onClick ? (ids) => {
|
|
3186
|
+
const deleteAction = bulkActions.find((action) => action.id === "delete");
|
|
3187
|
+
if (deleteAction) {
|
|
3188
|
+
deleteAction.onClick(ids);
|
|
3189
|
+
}
|
|
3190
|
+
} : void 0,
|
|
3191
|
+
tableRef,
|
|
3192
|
+
enabled: true
|
|
3193
|
+
});
|
|
3194
|
+
useEffect(() => {
|
|
3195
|
+
if (!searchConfig?.enabled || !searchConfig.preserveFocus) return;
|
|
3196
|
+
if (!shouldRestoreFocusRef.current) return;
|
|
3197
|
+
if (searchInputRef.current) {
|
|
3198
|
+
searchInputRef.current.focus();
|
|
3199
|
+
const len = searchConfig.value?.length ?? 0;
|
|
3200
|
+
try {
|
|
3201
|
+
searchInputRef.current.setSelectionRange(len, len);
|
|
3202
|
+
} catch {
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
shouldRestoreFocusRef.current = false;
|
|
3206
|
+
}, [searchConfig?.enabled, searchConfig?.preserveFocus, searchConfig?.value]);
|
|
3207
|
+
useEffect(() => {
|
|
3208
|
+
if (!urlPersistenceConfig?.enabled) return;
|
|
3209
|
+
if (!paginationConfig?.enabled) return;
|
|
3210
|
+
urlState.setPageToURL(pagination.currentPage);
|
|
3211
|
+
}, [pagination.currentPage, urlPersistenceConfig?.enabled, paginationConfig?.enabled, urlState]);
|
|
3212
|
+
useEffect(() => {
|
|
3213
|
+
if (!urlPersistenceConfig?.enabled) return;
|
|
3214
|
+
if (!filtersConfig?.enabled) return;
|
|
3215
|
+
urlState.setFiltersToURL(filters.filters);
|
|
3216
|
+
}, [filters.filters, urlPersistenceConfig?.enabled, filtersConfig?.enabled, urlState]);
|
|
3217
|
+
useEffect(() => {
|
|
3218
|
+
if (!urlPersistenceConfig?.enabled) return;
|
|
3219
|
+
if (!searchConfig?.enabled) return;
|
|
3220
|
+
if (!initialURLState?.search) return;
|
|
3221
|
+
if (!searchConfig.value && initialURLState.search) {
|
|
3222
|
+
searchConfig.onChange(initialURLState.search);
|
|
3223
|
+
}
|
|
3224
|
+
}, []);
|
|
3225
|
+
const allRowsSelected = useMemo(() => {
|
|
3226
|
+
if (sortedData.length === 0) return false;
|
|
3227
|
+
return sortedData.every((row) => selection.selectedIds.has(getRowId(row)));
|
|
3228
|
+
}, [sortedData, selection.selectedIds, getRowId]);
|
|
3229
|
+
const selectedDataForExport = useMemo(() => {
|
|
3230
|
+
if (!selectionConfig?.enabled || selection.selectedIds.size === 0) {
|
|
3231
|
+
return void 0;
|
|
3232
|
+
}
|
|
3233
|
+
return sortedData.filter((row) => selection.selectedIds.has(getRowId(row)));
|
|
3234
|
+
}, [sortedData, selection.selectedIds, getRowId, selectionConfig?.enabled]);
|
|
3235
|
+
const handleExecuteBulkAction = async (action) => {
|
|
3236
|
+
const selectedIds = selection.selectAllPages ? new Set(sortedData.map(getRowId)) : selection.selectedIds;
|
|
3237
|
+
if (action.confirmMessage) {
|
|
3238
|
+
const message = action.confirmMessage.replace("{count}", selectedIds.size.toString());
|
|
3239
|
+
if (!confirm(message)) {
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
if (action.maxSelection && selectedIds.size > action.maxSelection) {
|
|
3244
|
+
alert(`Maximum ${action.maxSelection} items can be selected for this action`);
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
try {
|
|
3248
|
+
await action.onClick(selectedIds);
|
|
3249
|
+
selection.clearSelection();
|
|
3250
|
+
} catch (error) {
|
|
3251
|
+
console.error("Bulk action failed:", error);
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
if (loading && sortedData.length === 0) {
|
|
3255
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex items-center justify-center p-12", className), children: /* @__PURE__ */ jsx(Loader2, { className: "h-8 w-8 animate-spin text-muted-foreground" }) });
|
|
3256
|
+
}
|
|
3257
|
+
if (!loading && sortedData.length === 0 && emptyState) {
|
|
3258
|
+
return /* @__PURE__ */ jsx("div", { className, children: emptyState });
|
|
3259
|
+
}
|
|
3260
|
+
return /* @__PURE__ */ jsxs("div", { ref: tableRef, className: cn("space-y-4", className), tabIndex: 0, children: [
|
|
3261
|
+
/* @__PURE__ */ jsx(
|
|
3262
|
+
StandardTableToolbar,
|
|
3263
|
+
{
|
|
3264
|
+
searchEnabled: searchConfig?.enabled,
|
|
3265
|
+
searchPlaceholder: searchConfig?.placeholder,
|
|
3266
|
+
searchValue: searchConfig?.value,
|
|
3267
|
+
onSearchChange: (value) => {
|
|
3268
|
+
if (searchConfig) {
|
|
3269
|
+
shouldRestoreFocusRef.current = true;
|
|
3270
|
+
searchConfig.onChange(value);
|
|
3271
|
+
urlState.setSearchToURL(value);
|
|
3272
|
+
}
|
|
3273
|
+
},
|
|
3274
|
+
searchInputRef,
|
|
3275
|
+
searchAutoFocus: searchConfig?.autoFocus,
|
|
3276
|
+
columnVisibilityEnabled: columnVisibilityConfig?.enabled,
|
|
3277
|
+
hideableColumns,
|
|
3278
|
+
columnVisibility,
|
|
3279
|
+
onToggleColumnVisibility: toggleColumnVisibility,
|
|
3280
|
+
exportEnabled: exportConfig?.enabled,
|
|
3281
|
+
exportData: sortedData,
|
|
3282
|
+
exportColumns: displayColumns,
|
|
3283
|
+
exportProps: exportConfig?.enabled ? {
|
|
3284
|
+
filteredData: sortedData.length < safeData.length ? sortedData : void 0,
|
|
3285
|
+
selectedData: selectedDataForExport,
|
|
3286
|
+
baseFilename: exportConfig.baseFilename,
|
|
3287
|
+
showProgress: exportConfig.showProgress,
|
|
3288
|
+
onExportStart: exportConfig.onExportStart,
|
|
3289
|
+
onExportComplete: exportConfig.onExportComplete,
|
|
3290
|
+
onExportError: exportConfig.onExportError
|
|
3291
|
+
} : void 0,
|
|
3292
|
+
savedViewsEnabled: savedViewsConfig?.enabled,
|
|
3293
|
+
savedViews: savedViewsConfig?.views,
|
|
3294
|
+
currentViewId: savedViewsConfig?.currentViewId,
|
|
3295
|
+
savedViewsProps: savedViewsConfig?.enabled ? {
|
|
3296
|
+
onSaveView: savedViewsConfig.onSaveView,
|
|
3297
|
+
onUpdateView: savedViewsConfig.onUpdateView,
|
|
3298
|
+
onDeleteView: savedViewsConfig.onDeleteView,
|
|
3299
|
+
onLoadView: savedViewsConfig.onLoadView || (() => {
|
|
3300
|
+
}),
|
|
3301
|
+
getCurrentViewState
|
|
3302
|
+
} : void 0
|
|
3303
|
+
}
|
|
3304
|
+
),
|
|
3305
|
+
filtersConfig?.enabled && filtersConfig.config && /* @__PURE__ */ jsx(TableFilters, { config: filtersConfig.config, filters }),
|
|
3306
|
+
selectionConfig?.enabled && bulkActions.length > 0 && /* @__PURE__ */ jsx(
|
|
3307
|
+
BulkActionBar,
|
|
3308
|
+
{
|
|
3309
|
+
selectedCount: selection.getSelectedCount(),
|
|
3310
|
+
selectAllPages: selection.selectAllPages,
|
|
3311
|
+
totalCount: paginationConfig?.totalCount || sortedData.length,
|
|
3312
|
+
bulkActions,
|
|
3313
|
+
onClearSelection: selection.clearSelection,
|
|
3314
|
+
onExecuteAction: handleExecuteBulkAction
|
|
3315
|
+
}
|
|
3316
|
+
),
|
|
3317
|
+
selectionConfig?.enabled && selection.getSelectedCount() > 0 && bulkActions.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [
|
|
3318
|
+
/* @__PURE__ */ jsx("div", { children: selection.selectAllPages ? /* @__PURE__ */ jsxs("span", { className: "font-medium text-blue-600", children: [
|
|
3319
|
+
"All ",
|
|
3320
|
+
paginationConfig?.totalCount || sortedData.length,
|
|
3321
|
+
" items selected"
|
|
3322
|
+
] }) : /* @__PURE__ */ jsxs("span", { children: [
|
|
3323
|
+
selection.getSelectedCount(),
|
|
3324
|
+
" item",
|
|
3325
|
+
selection.getSelectedCount() === 1 ? "" : "s",
|
|
3326
|
+
" selected",
|
|
3327
|
+
allRowsSelected ? " on this page" : ""
|
|
3328
|
+
] }) }),
|
|
3329
|
+
!selection.selectAllPages && allRowsSelected && paginationConfig && (paginationConfig.totalCount || 0) > sortedData.length && /* @__PURE__ */ jsxs(
|
|
3330
|
+
"button",
|
|
3331
|
+
{
|
|
3332
|
+
onClick: selection.selectAllPagesToggle,
|
|
3333
|
+
className: "text-blue-600 hover:text-blue-800 font-medium",
|
|
3334
|
+
children: [
|
|
3335
|
+
"Select all ",
|
|
3336
|
+
paginationConfig.totalCount,
|
|
3337
|
+
" items"
|
|
3338
|
+
]
|
|
3339
|
+
}
|
|
3340
|
+
)
|
|
3341
|
+
] }),
|
|
3342
|
+
selectionConfig?.enabled && bulkActions.length > 0 && !selection.selectAllPages && allRowsSelected && paginationConfig && (paginationConfig.totalCount || 0) > sortedData.length && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-2 bg-blue-50 border border-blue-200 rounded-lg", children: /* @__PURE__ */ jsxs(
|
|
3343
|
+
"button",
|
|
3344
|
+
{
|
|
3345
|
+
onClick: selection.selectAllPagesToggle,
|
|
3346
|
+
className: "text-blue-600 hover:text-blue-800 font-medium text-sm",
|
|
3347
|
+
children: [
|
|
3348
|
+
"Select all ",
|
|
3349
|
+
paginationConfig.totalCount,
|
|
3350
|
+
" items across all pages"
|
|
3351
|
+
]
|
|
3352
|
+
}
|
|
3353
|
+
) }),
|
|
3354
|
+
/* @__PURE__ */ jsx(
|
|
3355
|
+
DataTableCore,
|
|
3356
|
+
{
|
|
3357
|
+
data: sortedData,
|
|
3358
|
+
columns: displayColumns,
|
|
3359
|
+
getRowId,
|
|
3360
|
+
selectionEnabled: selectionConfig?.enabled,
|
|
3361
|
+
selectedIds: selection.selectedIds,
|
|
3362
|
+
onToggleRow: selection.toggleRow,
|
|
3363
|
+
onToggleAll: selection.toggleAll,
|
|
3364
|
+
allRowsSelected,
|
|
3365
|
+
renderSelectionCell: selectionConfig?.renderSelectionCell,
|
|
3366
|
+
sortingEnabled,
|
|
3367
|
+
sortBy: currentSortBy,
|
|
3368
|
+
sortDirection: currentSortDirection,
|
|
3369
|
+
onSort: handleSort,
|
|
3370
|
+
columnReorderEnabled: columnReorderConfig?.enabled,
|
|
3371
|
+
onColumnDragEnd: columnReorder.handleDragEnd,
|
|
3372
|
+
columnResizeEnabled: columnResizeConfig?.enabled,
|
|
3373
|
+
columnWidths: columnResize.columnWidths,
|
|
3374
|
+
onColumnResizeStart: columnResize.startResize,
|
|
3375
|
+
isResizing: columnResize.isResizing,
|
|
3376
|
+
inlineEditEnabled: inlineEditConfig?.enabled,
|
|
3377
|
+
onCellEdit: inlineEditConfig?.onSave,
|
|
3378
|
+
rowActions,
|
|
3379
|
+
onRowClick,
|
|
3380
|
+
loadingRows
|
|
3381
|
+
}
|
|
3382
|
+
),
|
|
3383
|
+
paginationConfig?.enabled && /* @__PURE__ */ jsx(
|
|
3384
|
+
TablePagination,
|
|
3385
|
+
{
|
|
3386
|
+
pagination,
|
|
3387
|
+
totalFilteredCount: paginationConfig.totalCount
|
|
3388
|
+
}
|
|
3389
|
+
)
|
|
3390
|
+
] });
|
|
3391
|
+
}
|
|
3392
|
+
var Card = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
3393
|
+
"div",
|
|
3394
|
+
{
|
|
3395
|
+
ref,
|
|
3396
|
+
className: cn(
|
|
3397
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
3398
|
+
className
|
|
3399
|
+
),
|
|
3400
|
+
...props
|
|
3401
|
+
}
|
|
3402
|
+
));
|
|
3403
|
+
Card.displayName = "Card";
|
|
3404
|
+
var CardHeader = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("flex flex-col space-y-1.5 p-6", className), ...props }));
|
|
3405
|
+
CardHeader.displayName = "CardHeader";
|
|
3406
|
+
var CardTitle = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
3407
|
+
"h3",
|
|
3408
|
+
{
|
|
3409
|
+
ref,
|
|
3410
|
+
className: cn("font-semibold leading-none tracking-tight", className),
|
|
3411
|
+
...props
|
|
3412
|
+
}
|
|
3413
|
+
));
|
|
3414
|
+
CardTitle.displayName = "CardTitle";
|
|
3415
|
+
var CardDescription = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
3416
|
+
"p",
|
|
3417
|
+
{
|
|
3418
|
+
ref,
|
|
3419
|
+
className: cn("text-sm text-muted-foreground", className),
|
|
3420
|
+
...props
|
|
3421
|
+
}
|
|
3422
|
+
));
|
|
3423
|
+
CardDescription.displayName = "CardDescription";
|
|
3424
|
+
var CardContent = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("p-6 pt-0", className), ...props }));
|
|
3425
|
+
CardContent.displayName = "CardContent";
|
|
3426
|
+
var CardFooter = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("flex items-center p-6 pt-0", className), ...props }));
|
|
3427
|
+
CardFooter.displayName = "CardFooter";
|
|
3428
|
+
function CardActions({
|
|
3429
|
+
actions,
|
|
3430
|
+
row,
|
|
3431
|
+
className,
|
|
3432
|
+
maxVisibleActions = 2
|
|
3433
|
+
}) {
|
|
3434
|
+
const [isExecuting, setIsExecuting] = useState(null);
|
|
3435
|
+
const visibleActions = actions.filter((action) => {
|
|
3436
|
+
return !(action.hidden && action.hidden(row));
|
|
3437
|
+
});
|
|
3438
|
+
if (visibleActions.length === 0) {
|
|
3439
|
+
return null;
|
|
3440
|
+
}
|
|
3441
|
+
const handleActionClick = async (action) => {
|
|
3442
|
+
if (action.disabled && action.disabled(row)) {
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
setIsExecuting(action.id);
|
|
3446
|
+
try {
|
|
3447
|
+
await action.onClick(row);
|
|
3448
|
+
} finally {
|
|
3449
|
+
setIsExecuting(null);
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
3452
|
+
const primaryActions = visibleActions.slice(0, maxVisibleActions);
|
|
3453
|
+
const overflowActions = visibleActions.slice(maxVisibleActions);
|
|
3454
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1", className), children: [
|
|
3455
|
+
primaryActions.map((action) => {
|
|
3456
|
+
const Icon2 = action.icon;
|
|
3457
|
+
const isDisabled = action.disabled ? action.disabled(row) : false;
|
|
3458
|
+
const isLoading = isExecuting === action.id;
|
|
3459
|
+
return /* @__PURE__ */ jsxs(
|
|
3460
|
+
Button,
|
|
3461
|
+
{
|
|
3462
|
+
variant: action.variant || "ghost",
|
|
3463
|
+
size: "sm",
|
|
3464
|
+
className: cn(
|
|
3465
|
+
"h-9 min-w-[44px]",
|
|
3466
|
+
Icon2 && !action.label && "w-9 p-0",
|
|
3467
|
+
action.className
|
|
3468
|
+
),
|
|
3469
|
+
onClick: (e) => {
|
|
3470
|
+
e.stopPropagation();
|
|
3471
|
+
handleActionClick(action);
|
|
3472
|
+
},
|
|
3473
|
+
disabled: isDisabled || isLoading,
|
|
3474
|
+
children: [
|
|
3475
|
+
Icon2 && /* @__PURE__ */ jsx(Icon2, { className: cn(
|
|
3476
|
+
"h-4 w-4",
|
|
3477
|
+
action.label && "mr-1.5"
|
|
3478
|
+
) }),
|
|
3479
|
+
action.label && /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: action.label })
|
|
3480
|
+
]
|
|
3481
|
+
},
|
|
3482
|
+
action.id
|
|
3483
|
+
);
|
|
3484
|
+
}),
|
|
3485
|
+
overflowActions.length > 0 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
3486
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
3487
|
+
Button,
|
|
3488
|
+
{
|
|
3489
|
+
variant: "ghost",
|
|
3490
|
+
size: "sm",
|
|
3491
|
+
className: "h-9 w-9 p-0",
|
|
3492
|
+
onClick: (e) => e.stopPropagation(),
|
|
3493
|
+
children: [
|
|
3494
|
+
/* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" }),
|
|
3495
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "More actions" })
|
|
3496
|
+
]
|
|
3497
|
+
}
|
|
3498
|
+
) }),
|
|
3499
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-48", children: overflowActions.map((action, index) => {
|
|
3500
|
+
const Icon2 = action.icon;
|
|
3501
|
+
const isDisabled = action.disabled ? action.disabled(row) : false;
|
|
3502
|
+
const isLoading = isExecuting === action.id;
|
|
3503
|
+
return /* @__PURE__ */ jsxs(
|
|
3504
|
+
DropdownMenuItem,
|
|
3505
|
+
{
|
|
3506
|
+
onClick: (e) => {
|
|
3507
|
+
e.stopPropagation();
|
|
3508
|
+
handleActionClick(action);
|
|
3509
|
+
},
|
|
3510
|
+
disabled: isDisabled || isLoading,
|
|
3511
|
+
className: cn(
|
|
3512
|
+
action.variant === "destructive" && "text-destructive focus:text-destructive",
|
|
3513
|
+
action.className
|
|
3514
|
+
),
|
|
3515
|
+
children: [
|
|
3516
|
+
Icon2 && /* @__PURE__ */ jsx(Icon2, { className: "mr-2 h-4 w-4" }),
|
|
3517
|
+
/* @__PURE__ */ jsx("span", { children: action.label || "Action" })
|
|
3518
|
+
]
|
|
3519
|
+
},
|
|
3520
|
+
action.id
|
|
3521
|
+
);
|
|
3522
|
+
}) })
|
|
3523
|
+
] })
|
|
3524
|
+
] });
|
|
3525
|
+
}
|
|
3526
|
+
function getNestedValue(obj, path) {
|
|
3527
|
+
if (!path || !obj) return obj;
|
|
3528
|
+
const keys = path.split(".");
|
|
3529
|
+
let value = obj;
|
|
3530
|
+
for (const key of keys) {
|
|
3531
|
+
if (value === null || value === void 0) return void 0;
|
|
3532
|
+
value = value[key];
|
|
3533
|
+
}
|
|
3534
|
+
return value;
|
|
3535
|
+
}
|
|
3536
|
+
function renderField(field, row) {
|
|
3537
|
+
const value = getNestedValue(row, field.key);
|
|
3538
|
+
if (field.render) {
|
|
3539
|
+
return field.render(value, row);
|
|
3540
|
+
}
|
|
3541
|
+
if (value === null || value === void 0) {
|
|
3542
|
+
return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
3543
|
+
}
|
|
3544
|
+
if (typeof value === "boolean") {
|
|
3545
|
+
return /* @__PURE__ */ jsx("span", { children: value ? "Yes" : "No" });
|
|
3546
|
+
}
|
|
3547
|
+
return /* @__PURE__ */ jsx("span", { children: String(value) });
|
|
3548
|
+
}
|
|
3549
|
+
function Card2({
|
|
3550
|
+
row,
|
|
3551
|
+
config,
|
|
3552
|
+
rowId,
|
|
3553
|
+
isLoading = false,
|
|
3554
|
+
className
|
|
3555
|
+
}) {
|
|
3556
|
+
const {
|
|
3557
|
+
titleKey,
|
|
3558
|
+
titleRender,
|
|
3559
|
+
subtitleKey,
|
|
3560
|
+
subtitleRender,
|
|
3561
|
+
imageKey,
|
|
3562
|
+
imageRender,
|
|
3563
|
+
primaryFields,
|
|
3564
|
+
secondaryFields,
|
|
3565
|
+
actions,
|
|
3566
|
+
renderCustomContent,
|
|
3567
|
+
renderCustomHeader,
|
|
3568
|
+
renderCustomFooter,
|
|
3569
|
+
showSelection,
|
|
3570
|
+
onSelectionChange,
|
|
3571
|
+
isSelected,
|
|
3572
|
+
onClick,
|
|
3573
|
+
headerClassName,
|
|
3574
|
+
contentClassName,
|
|
3575
|
+
footerClassName
|
|
3576
|
+
} = config;
|
|
3577
|
+
const handleClick = () => {
|
|
3578
|
+
if (onClick && !isLoading) {
|
|
3579
|
+
onClick(row);
|
|
3580
|
+
}
|
|
3581
|
+
};
|
|
3582
|
+
const handleSelectionChange = (checked) => {
|
|
3583
|
+
if (onSelectionChange && rowId) {
|
|
3584
|
+
onSelectionChange(rowId, checked);
|
|
3585
|
+
}
|
|
3586
|
+
};
|
|
3587
|
+
const selected = isSelected ? isSelected(row) : false;
|
|
3588
|
+
return /* @__PURE__ */ jsxs(
|
|
3589
|
+
Card,
|
|
3590
|
+
{
|
|
3591
|
+
className: cn(
|
|
3592
|
+
"relative transition-all duration-200",
|
|
3593
|
+
onClick && !isLoading && "cursor-pointer hover:shadow-md hover:border-primary/50",
|
|
3594
|
+
selected && "ring-2 ring-primary border-primary",
|
|
3595
|
+
isLoading && "opacity-60 pointer-events-none",
|
|
3596
|
+
className,
|
|
3597
|
+
config.className
|
|
3598
|
+
),
|
|
3599
|
+
onClick: handleClick,
|
|
3600
|
+
children: [
|
|
3601
|
+
isLoading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-background/50 rounded-lg z-10", children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-primary" }) }),
|
|
3602
|
+
/* @__PURE__ */ jsx(CardHeader, { className: cn("pb-3", headerClassName), children: renderCustomHeader ? renderCustomHeader(row) : /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
3603
|
+
showSelection && /* @__PURE__ */ jsx(
|
|
3604
|
+
"div",
|
|
3605
|
+
{
|
|
3606
|
+
className: "flex items-center pt-1",
|
|
3607
|
+
onClick: (e) => e.stopPropagation(),
|
|
3608
|
+
children: /* @__PURE__ */ jsx(
|
|
3609
|
+
Checkbox,
|
|
3610
|
+
{
|
|
3611
|
+
checked: selected,
|
|
3612
|
+
onCheckedChange: handleSelectionChange,
|
|
3613
|
+
"aria-label": "Select row",
|
|
3614
|
+
className: "h-5 w-5"
|
|
3615
|
+
}
|
|
3616
|
+
)
|
|
3617
|
+
}
|
|
3618
|
+
),
|
|
3619
|
+
(imageKey || imageRender) && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0", children: imageRender ? imageRender(row) : /* @__PURE__ */ jsx("div", { className: "h-12 w-12 rounded-full bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-lg font-semibold text-primary", children: String(getNestedValue(row, titleKey) || "?").charAt(0).toUpperCase() }) }) }),
|
|
3620
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
3621
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
3622
|
+
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-base leading-tight truncate", children: titleRender ? titleRender(row) : getNestedValue(row, titleKey) }),
|
|
3623
|
+
subtitleKey && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 truncate", children: subtitleRender ? subtitleRender(row) : getNestedValue(row, subtitleKey) })
|
|
3624
|
+
] }),
|
|
3625
|
+
actions && actions.length > 0 && /* @__PURE__ */ jsx("div", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
3626
|
+
CardActions,
|
|
3627
|
+
{
|
|
3628
|
+
actions,
|
|
3629
|
+
row,
|
|
3630
|
+
maxVisibleActions: 2
|
|
3631
|
+
}
|
|
3632
|
+
) })
|
|
3633
|
+
] }) })
|
|
3634
|
+
] }) }),
|
|
3635
|
+
/* @__PURE__ */ jsx(CardContent, { className: cn("pt-0 space-y-4", contentClassName), children: renderCustomContent ? renderCustomContent(row) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3636
|
+
primaryFields && primaryFields.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-3", children: primaryFields.map((field) => /* @__PURE__ */ jsxs(
|
|
3637
|
+
"div",
|
|
3638
|
+
{
|
|
3639
|
+
className: cn(
|
|
3640
|
+
"flex justify-between items-center gap-2",
|
|
3641
|
+
field.className
|
|
3642
|
+
),
|
|
3643
|
+
children: [
|
|
3644
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-muted-foreground flex-shrink-0", children: [
|
|
3645
|
+
field.label || field.key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()),
|
|
3646
|
+
":"
|
|
3647
|
+
] }),
|
|
3648
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-right truncate", children: renderField(field, row) })
|
|
3649
|
+
]
|
|
3650
|
+
},
|
|
3651
|
+
field.key
|
|
3652
|
+
)) }),
|
|
3653
|
+
secondaryFields && secondaryFields.length > 0 && /* @__PURE__ */ jsx("div", { className: "border-t pt-3", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3", children: secondaryFields.map((field) => /* @__PURE__ */ jsxs(
|
|
3654
|
+
"div",
|
|
3655
|
+
{
|
|
3656
|
+
className: cn("space-y-1", field.className),
|
|
3657
|
+
children: [
|
|
3658
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: field.label || field.key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()) }),
|
|
3659
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold truncate", children: renderField(field, row) })
|
|
3660
|
+
]
|
|
3661
|
+
},
|
|
3662
|
+
field.key
|
|
3663
|
+
)) }) })
|
|
3664
|
+
] }) }),
|
|
3665
|
+
renderCustomFooter && /* @__PURE__ */ jsx(CardFooter, { className: cn("pt-0", footerClassName), children: renderCustomFooter(row) })
|
|
3666
|
+
]
|
|
3667
|
+
}
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// src/components/unified-table/components/MobileView/types.ts
|
|
3672
|
+
var MOBILE_BREAKPOINT = 768;
|
|
3673
|
+
function MobileView({
|
|
3674
|
+
data,
|
|
3675
|
+
config,
|
|
3676
|
+
getRowId,
|
|
3677
|
+
loading = false,
|
|
3678
|
+
loadingRows,
|
|
3679
|
+
emptyState,
|
|
3680
|
+
className
|
|
3681
|
+
}) {
|
|
3682
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
3683
|
+
useEffect(() => {
|
|
3684
|
+
const checkMobile = () => {
|
|
3685
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
3686
|
+
};
|
|
3687
|
+
checkMobile();
|
|
3688
|
+
window.addEventListener("resize", checkMobile);
|
|
3689
|
+
return () => window.removeEventListener("resize", checkMobile);
|
|
3690
|
+
}, []);
|
|
3691
|
+
if (data.length === 0 && !loading) {
|
|
3692
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex items-center justify-center py-12", className), children: emptyState || /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "No data to display" }) }) });
|
|
3693
|
+
}
|
|
3694
|
+
return /* @__PURE__ */ jsx(
|
|
3695
|
+
"div",
|
|
3696
|
+
{
|
|
3697
|
+
className: cn(
|
|
3698
|
+
"space-y-4",
|
|
3699
|
+
isMobile && "space-y-3",
|
|
3700
|
+
className
|
|
3701
|
+
),
|
|
3702
|
+
children: data.map((row, index) => {
|
|
3703
|
+
const rowId = getRowId ? getRowId(row) : String(index);
|
|
3704
|
+
const isRowLoading = loadingRows?.has(rowId) || false;
|
|
3705
|
+
return /* @__PURE__ */ jsx(
|
|
3706
|
+
Card2,
|
|
3707
|
+
{
|
|
3708
|
+
row,
|
|
3709
|
+
config,
|
|
3710
|
+
rowId,
|
|
3711
|
+
isLoading: isRowLoading
|
|
3712
|
+
},
|
|
3713
|
+
rowId
|
|
3714
|
+
);
|
|
3715
|
+
})
|
|
3716
|
+
}
|
|
3717
|
+
);
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
// src/components/unified-table/utils/renderers.ts
|
|
3721
|
+
function createCellRenderer(render) {
|
|
3722
|
+
return render;
|
|
3723
|
+
}
|
|
3724
|
+
function getNestedValue2(obj, path) {
|
|
3725
|
+
if (!path || !obj) return obj;
|
|
3726
|
+
const keys = path.split(".");
|
|
3727
|
+
let value = obj;
|
|
3728
|
+
for (const key of keys) {
|
|
3729
|
+
if (value === null || value === void 0) return void 0;
|
|
3730
|
+
value = value[key];
|
|
3731
|
+
}
|
|
3732
|
+
return value;
|
|
3733
|
+
}
|
|
3734
|
+
var commonRenderers = {
|
|
3735
|
+
text: createCellRenderer((value) => String(value || "-")),
|
|
3736
|
+
number: createCellRenderer((value) => {
|
|
3737
|
+
if (value === null || value === void 0) return "-";
|
|
3738
|
+
return new Intl.NumberFormat().format(Number(value));
|
|
3739
|
+
}),
|
|
3740
|
+
currency: createCellRenderer((value, row, options) => {
|
|
3741
|
+
if (value === null || value === void 0) return "-";
|
|
3742
|
+
return new Intl.NumberFormat(options?.locale || "en-US", {
|
|
3743
|
+
style: "currency",
|
|
3744
|
+
currency: options?.currency || "USD"
|
|
3745
|
+
}).format(Number(value));
|
|
3746
|
+
}),
|
|
3747
|
+
percentage: createCellRenderer((value) => {
|
|
3748
|
+
if (value === null || value === void 0) return "-";
|
|
3749
|
+
return `${Number(value).toFixed(2)}%`;
|
|
3750
|
+
}),
|
|
3751
|
+
date: createCellRenderer((value) => {
|
|
3752
|
+
if (!value) return "-";
|
|
3753
|
+
const date = new Date(value);
|
|
3754
|
+
return date.toLocaleDateString();
|
|
3755
|
+
}),
|
|
3756
|
+
datetime: createCellRenderer((value) => {
|
|
3757
|
+
if (!value) return "-";
|
|
3758
|
+
const date = new Date(value);
|
|
3759
|
+
return date.toLocaleString();
|
|
3760
|
+
}),
|
|
3761
|
+
boolean: createCellRenderer((value) => {
|
|
3762
|
+
if (value === null || value === void 0) return "-";
|
|
3763
|
+
return value ? "Yes" : "No";
|
|
3764
|
+
}),
|
|
3765
|
+
badge: createCellRenderer((value, row, options) => {
|
|
3766
|
+
if (!value) return null;
|
|
3767
|
+
return value;
|
|
3768
|
+
}),
|
|
3769
|
+
truncate: createCellRenderer((value, row, options) => {
|
|
3770
|
+
if (!value) return "-";
|
|
3771
|
+
const str = String(value);
|
|
3772
|
+
const maxLength = options?.maxLength || 50;
|
|
3773
|
+
if (str.length <= maxLength) return str;
|
|
3774
|
+
return `${str.substring(0, maxLength)}...`;
|
|
3775
|
+
}),
|
|
3776
|
+
array: createCellRenderer((value, row, options) => {
|
|
3777
|
+
if (!Array.isArray(value) || value.length === 0) return "-";
|
|
3778
|
+
const maxItems = options?.maxItems || value.length;
|
|
3779
|
+
const items = value.slice(0, maxItems);
|
|
3780
|
+
const separator = options?.separator || ", ";
|
|
3781
|
+
const display = items.join(separator);
|
|
3782
|
+
if (value.length > maxItems) {
|
|
3783
|
+
return `${display} +${value.length - maxItems} more`;
|
|
3784
|
+
}
|
|
3785
|
+
return display;
|
|
3786
|
+
})
|
|
3787
|
+
};
|
|
3788
|
+
function combineRenderers(...renderers) {
|
|
3789
|
+
return (value, row) => {
|
|
3790
|
+
for (const renderer of renderers) {
|
|
3791
|
+
const result = renderer(value, row);
|
|
3792
|
+
if (result !== null && result !== void 0) {
|
|
3793
|
+
return result;
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
return value;
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
// src/components/unified-table/utils/themes.ts
|
|
3801
|
+
var defaultTheme = {
|
|
3802
|
+
name: "default",
|
|
3803
|
+
container: "w-full",
|
|
3804
|
+
header: "bg-muted/50",
|
|
3805
|
+
headerCell: "px-4 py-3 text-left text-sm font-medium text-muted-foreground",
|
|
3806
|
+
body: "",
|
|
3807
|
+
row: "border-b transition-colors",
|
|
3808
|
+
rowHover: "hover:bg-muted/50",
|
|
3809
|
+
rowSelected: "bg-primary/10 border-primary",
|
|
3810
|
+
cell: "px-4 py-3 text-sm",
|
|
3811
|
+
pagination: "flex items-center justify-between px-4 py-3 border-t",
|
|
3812
|
+
emptyState: "flex items-center justify-center py-12 text-muted-foreground",
|
|
3813
|
+
mobileCard: "rounded-lg border bg-card shadow-sm",
|
|
3814
|
+
mobileCardHeader: "p-4 pb-2",
|
|
3815
|
+
mobileCardContent: "p-4 pt-2"
|
|
3816
|
+
};
|
|
3817
|
+
var compactTheme = {
|
|
3818
|
+
...defaultTheme,
|
|
3819
|
+
name: "compact",
|
|
3820
|
+
headerCell: "px-2 py-2 text-left text-xs font-medium text-muted-foreground",
|
|
3821
|
+
cell: "px-2 py-2 text-xs",
|
|
3822
|
+
pagination: "flex items-center justify-between px-2 py-2 border-t",
|
|
3823
|
+
mobileCardHeader: "p-3 pb-1",
|
|
3824
|
+
mobileCardContent: "p-3 pt-1"
|
|
3825
|
+
};
|
|
3826
|
+
var spaciousTheme = {
|
|
3827
|
+
...defaultTheme,
|
|
3828
|
+
name: "spacious",
|
|
3829
|
+
headerCell: "px-6 py-4 text-left text-sm font-medium text-muted-foreground",
|
|
3830
|
+
cell: "px-6 py-4 text-sm",
|
|
3831
|
+
pagination: "flex items-center justify-between px-6 py-4 border-t",
|
|
3832
|
+
mobileCardHeader: "p-6 pb-3",
|
|
3833
|
+
mobileCardContent: "p-6 pt-3"
|
|
3834
|
+
};
|
|
3835
|
+
var minimalTheme = {
|
|
3836
|
+
...defaultTheme,
|
|
3837
|
+
name: "minimal",
|
|
3838
|
+
header: "",
|
|
3839
|
+
headerCell: "px-4 py-3 text-left text-sm font-semibold",
|
|
3840
|
+
row: "border-b border-border/50",
|
|
3841
|
+
rowHover: "hover:bg-muted/30",
|
|
3842
|
+
mobileCard: "rounded-lg border border-border/50 bg-card"
|
|
3843
|
+
};
|
|
3844
|
+
var themes = {
|
|
3845
|
+
default: defaultTheme,
|
|
3846
|
+
compact: compactTheme,
|
|
3847
|
+
spacious: spaciousTheme,
|
|
3848
|
+
minimal: minimalTheme
|
|
3849
|
+
};
|
|
3850
|
+
function getTheme(themeName = "default") {
|
|
3851
|
+
return themes[themeName] || defaultTheme;
|
|
3852
|
+
}
|
|
3853
|
+
function createCustomTheme(baseTheme = "default", overrides) {
|
|
3854
|
+
return {
|
|
3855
|
+
...themes[baseTheme],
|
|
3856
|
+
...overrides,
|
|
3857
|
+
name: overrides.name || `custom-${baseTheme}`
|
|
3858
|
+
};
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
// src/components/unified-table/utils/validation.ts
|
|
3862
|
+
function validateMobileCardConfig(config) {
|
|
3863
|
+
const errors = [];
|
|
3864
|
+
const warnings = [];
|
|
3865
|
+
if (!config.titleKey && !config.titleRender) {
|
|
3866
|
+
errors.push("Mobile card config must have either titleKey or titleRender");
|
|
3867
|
+
}
|
|
3868
|
+
if (!config.primaryFields || config.primaryFields.length === 0) {
|
|
3869
|
+
warnings.push("Mobile card config has no primary fields defined");
|
|
3870
|
+
}
|
|
3871
|
+
if (config.primaryFields) {
|
|
3872
|
+
config.primaryFields.forEach((field, index) => {
|
|
3873
|
+
if (!field.key && !field.render) {
|
|
3874
|
+
errors.push(`Primary field at index ${index} must have either key or render function`);
|
|
3875
|
+
}
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
if (config.secondaryFields) {
|
|
3879
|
+
config.secondaryFields.forEach((field, index) => {
|
|
3880
|
+
if (!field.key && !field.render) {
|
|
3881
|
+
errors.push(`Secondary field at index ${index} must have either key or render function`);
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
if (config.actions) {
|
|
3886
|
+
config.actions.forEach((action, index) => {
|
|
3887
|
+
if (!action.id) {
|
|
3888
|
+
errors.push(`Action at index ${index} must have an id`);
|
|
3889
|
+
}
|
|
3890
|
+
if (!action.onClick) {
|
|
3891
|
+
errors.push(`Action '${action.id || index}' must have an onClick handler`);
|
|
3892
|
+
}
|
|
3893
|
+
if (!action.label && !action.icon) {
|
|
3894
|
+
warnings.push(`Action '${action.id || index}' should have either a label or icon`);
|
|
3895
|
+
}
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
if (config.showSelection && !config.onSelectionChange) {
|
|
3899
|
+
warnings.push("showSelection is enabled but onSelectionChange handler is not provided");
|
|
3900
|
+
}
|
|
3901
|
+
return {
|
|
3902
|
+
valid: errors.length === 0,
|
|
3903
|
+
errors,
|
|
3904
|
+
warnings
|
|
3905
|
+
};
|
|
3906
|
+
}
|
|
3907
|
+
function validateColumnConfig(columns) {
|
|
3908
|
+
const errors = [];
|
|
3909
|
+
const warnings = [];
|
|
3910
|
+
if (!columns || columns.length === 0) {
|
|
3911
|
+
errors.push("At least one column must be defined");
|
|
3912
|
+
return { valid: false, errors, warnings };
|
|
3913
|
+
}
|
|
3914
|
+
const columnIds = /* @__PURE__ */ new Set();
|
|
3915
|
+
columns.forEach((column, index) => {
|
|
3916
|
+
if (!column.id && !column.accessorKey) {
|
|
3917
|
+
errors.push(`Column at index ${index} must have either id or accessorKey`);
|
|
3918
|
+
}
|
|
3919
|
+
const columnId = column.id || column.accessorKey;
|
|
3920
|
+
if (columnIds.has(columnId)) {
|
|
3921
|
+
errors.push(`Duplicate column id: '${columnId}'`);
|
|
3922
|
+
}
|
|
3923
|
+
columnIds.add(columnId);
|
|
3924
|
+
if (!column.header && !column.hideHeader) {
|
|
3925
|
+
warnings.push(`Column '${columnId}' has no header defined`);
|
|
3926
|
+
}
|
|
3927
|
+
if (!column.cell && !column.accessorKey && !column.accessorFn) {
|
|
3928
|
+
warnings.push(`Column '${columnId}' has no way to access data (missing cell, accessorKey, or accessorFn)`);
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
return {
|
|
3932
|
+
valid: errors.length === 0,
|
|
3933
|
+
errors,
|
|
3934
|
+
warnings
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
function logValidationResults(configName, result, throwOnError = false) {
|
|
3938
|
+
if (result.errors.length > 0) {
|
|
3939
|
+
console.error(`[${configName}] Validation Errors:`, result.errors);
|
|
3940
|
+
if (throwOnError) {
|
|
3941
|
+
throw new Error(`${configName} validation failed: ${result.errors.join(", ")}`);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
if (result.warnings.length > 0) {
|
|
3945
|
+
console.warn(`[${configName}] Validation Warnings:`, result.warnings);
|
|
3946
|
+
}
|
|
3947
|
+
if (result.valid && result.warnings.length === 0) {
|
|
3948
|
+
console.log(`[${configName}] Configuration is valid`);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
export { Badge, Button, Card, Card2, CardActions, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Label, MOBILE_BREAKPOINT, MobileView, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, StandardTableToolbar, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, UnifiedTable, badgeVariants, buttonVariants, combineRenderers, commonRenderers, compactTheme, createCellRenderer, createCustomTheme, defaultTheme, exportData, exportToCSV, exportToExcel, generateExportFilename, getNestedValue2 as getNestedValue, getTheme, logValidationResults, minimalTheme, spaciousTheme, themes, useColumnReorder, useColumnResize, useColumnVisibility, useFilters, usePagination, useResponsive, useSelection, useTableKeyboard, useTablePreferences, useTableState, useTableURL, validateColumnConfig, validateMobileCardConfig };
|
|
3953
|
+
//# sourceMappingURL=chunk-27YUQBOE.mjs.map
|
|
3954
|
+
//# sourceMappingURL=chunk-27YUQBOE.mjs.map
|