@majapisoftwares/ui 5.0.0 → 6.0.0
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/AGENTS.md +11 -1
- package/README.md +19 -11
- package/THIRD_PARTY_NOTICES.md +25 -1
- package/components.json +1 -1
- package/docs/MIGRATION-6.0.md +33 -0
- package/docs/STANDARDS.md +11 -6
- package/docs/VERSIONING.md +28 -0
- package/package.json +19 -10
- package/scripts/check-standards.mjs +2 -0
- package/src/components/ui/accordion.tsx +82 -0
- package/src/components/ui/alert-dialog.tsx +187 -0
- package/src/components/ui/alert.tsx +2 -2
- package/src/components/ui/avatar.tsx +108 -0
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +125 -0
- package/src/components/ui/button-group.tsx +89 -0
- package/src/components/ui/button.tsx +12 -16
- package/src/components/ui/card.tsx +101 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +161 -0
- package/src/components/ui/dropdown-menu.tsx +271 -0
- package/src/components/ui/empty.tsx +4 -4
- package/src/components/ui/field.tsx +6 -6
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +155 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/orb.css +1002 -0
- package/src/components/ui/orb.tsx +752 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +204 -0
- package/src/components/ui/sheet.tsx +8 -6
- package/src/components/ui/sidebar.tsx +1035 -0
- package/src/components/ui/skeleton.tsx +1 -1
- package/src/components/ui/slider.tsx +53 -0
- package/src/components/ui/sonner.tsx +44 -0
- package/src/components/ui/spinner.tsx +32 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +10 -4
- package/src/components/ui/tabs.tsx +2 -2
- package/src/components/ui/textarea.tsx +19 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/lib/i18n.tsx +109 -0
- package/src/patterns/app-shell.tsx +158 -53
- package/src/patterns/data-table.tsx +2441 -180
- package/src/patterns/details-sheet.tsx +8 -6
- package/src/patterns/filter-tabs.tsx +53 -0
- package/src/patterns/majapi-logo.tsx +32 -0
- package/src/patterns/page-header.tsx +44 -22
- package/src/patterns/preferences.tsx +172 -0
- package/src/patterns/status-badge.tsx +4 -3
- package/src/styles.css +41 -341
- package/src/templates/admin-dashboard.tsx +14 -33
- package/src/templates/auth-layout.tsx +18 -24
- package/src/templates/login-form.tsx +97 -65
- package/src/templates/signup-form.tsx +139 -99
- package/src/components/ui/native-select.tsx +0 -66
- package/src/vite-env.d.ts +0 -1
|
@@ -1,15 +1,112 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
"use no memo";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
// Ported from the Numa app's data table (majapisoftwares/numa); this is now the
|
|
5
|
+
// canonical copy. Effects below intentionally sync restored localStorage
|
|
6
|
+
// preferences and incoming props into table state.
|
|
7
|
+
/* eslint-disable react-hooks/set-state-in-effect */
|
|
8
|
+
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import {
|
|
11
|
+
closestCenter,
|
|
12
|
+
DndContext,
|
|
13
|
+
KeyboardSensor,
|
|
14
|
+
MouseSensor,
|
|
15
|
+
TouchSensor,
|
|
16
|
+
useSensor,
|
|
17
|
+
useSensors,
|
|
18
|
+
type DragEndEvent,
|
|
19
|
+
} from "@dnd-kit/core";
|
|
20
|
+
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
|
4
21
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
arrayMove,
|
|
23
|
+
horizontalListSortingStrategy,
|
|
24
|
+
SortableContext,
|
|
25
|
+
useSortable,
|
|
26
|
+
} from "@dnd-kit/sortable";
|
|
27
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
28
|
+
import {
|
|
29
|
+
CircleAlertIcon,
|
|
30
|
+
InboxIcon,
|
|
31
|
+
LockIcon,
|
|
32
|
+
RotateCwIcon,
|
|
33
|
+
TriangleAlertIcon,
|
|
34
|
+
ArrowDownWideNarrowIcon,
|
|
35
|
+
ArrowUpDownIcon,
|
|
36
|
+
ArrowUpNarrowWideIcon,
|
|
37
|
+
CheckIcon,
|
|
38
|
+
ChevronLeftIcon,
|
|
39
|
+
ChevronRightIcon,
|
|
40
|
+
ChevronsLeftIcon,
|
|
41
|
+
ChevronsRightIcon,
|
|
42
|
+
Columns3Icon,
|
|
43
|
+
EyeOffIcon,
|
|
44
|
+
FunnelIcon,
|
|
45
|
+
FunnelXIcon,
|
|
46
|
+
GripVerticalIcon,
|
|
47
|
+
SearchIcon,
|
|
48
|
+
XIcon,
|
|
10
49
|
} from "lucide-react";
|
|
50
|
+
import {
|
|
51
|
+
createTable,
|
|
52
|
+
flexRender,
|
|
53
|
+
getCoreRowModel,
|
|
54
|
+
getFilteredRowModel,
|
|
55
|
+
getPaginationRowModel,
|
|
56
|
+
getSortedRowModel,
|
|
57
|
+
type Column,
|
|
58
|
+
type ColumnDef,
|
|
59
|
+
type ColumnFiltersState,
|
|
60
|
+
type ColumnOrderState,
|
|
61
|
+
type ColumnSizingState,
|
|
62
|
+
type FilterFn,
|
|
63
|
+
type Header,
|
|
64
|
+
type OnChangeFn,
|
|
65
|
+
type PaginationState,
|
|
66
|
+
type RowData,
|
|
67
|
+
type RowSelectionState,
|
|
68
|
+
type SortingState,
|
|
69
|
+
type Table as TanstackTable,
|
|
70
|
+
type TableOptions,
|
|
71
|
+
type TableOptionsResolved,
|
|
72
|
+
type VisibilityState,
|
|
73
|
+
} from "@tanstack/react-table";
|
|
74
|
+
|
|
75
|
+
import { Badge } from "../components/ui/badge";
|
|
11
76
|
import { Button } from "../components/ui/button";
|
|
77
|
+
import { Checkbox } from "../components/ui/checkbox";
|
|
78
|
+
import { FilterTabs } from "./filter-tabs";
|
|
79
|
+
import {
|
|
80
|
+
DropdownMenu,
|
|
81
|
+
DropdownMenuCheckboxItem,
|
|
82
|
+
DropdownMenuContent,
|
|
83
|
+
DropdownMenuGroup,
|
|
84
|
+
DropdownMenuItem,
|
|
85
|
+
DropdownMenuLabel,
|
|
86
|
+
DropdownMenuSeparator,
|
|
87
|
+
DropdownMenuTrigger,
|
|
88
|
+
} from "../components/ui/dropdown-menu";
|
|
12
89
|
import { Input } from "../components/ui/input";
|
|
90
|
+
import {
|
|
91
|
+
InputGroup,
|
|
92
|
+
InputGroupAddon,
|
|
93
|
+
InputGroupInput,
|
|
94
|
+
} from "../components/ui/input-group";
|
|
95
|
+
import { Label } from "../components/ui/label";
|
|
96
|
+
import {
|
|
97
|
+
Popover,
|
|
98
|
+
PopoverContent,
|
|
99
|
+
PopoverTrigger,
|
|
100
|
+
} from "../components/ui/popover";
|
|
101
|
+
import {
|
|
102
|
+
Select,
|
|
103
|
+
SelectContent,
|
|
104
|
+
SelectItem,
|
|
105
|
+
SelectTrigger,
|
|
106
|
+
SelectValue,
|
|
107
|
+
} from "../components/ui/select";
|
|
108
|
+
import { Separator } from "../components/ui/separator";
|
|
109
|
+
import { Skeleton } from "../components/ui/skeleton";
|
|
13
110
|
import {
|
|
14
111
|
Table,
|
|
15
112
|
TableBody,
|
|
@@ -18,208 +115,2372 @@ import {
|
|
|
18
115
|
TableHeader,
|
|
19
116
|
TableRow,
|
|
20
117
|
} from "../components/ui/table";
|
|
118
|
+
import {
|
|
119
|
+
Tooltip,
|
|
120
|
+
TooltipContent,
|
|
121
|
+
TooltipProvider,
|
|
122
|
+
TooltipTrigger,
|
|
123
|
+
} from "../components/ui/tooltip";
|
|
124
|
+
import { cn } from "cn";
|
|
125
|
+
import { defineMessages, useMessages, useUiMessages } from "../lib/i18n";
|
|
21
126
|
import {
|
|
22
127
|
Empty,
|
|
128
|
+
EmptyContent,
|
|
23
129
|
EmptyDescription,
|
|
24
130
|
EmptyHeader,
|
|
131
|
+
EmptyMedia,
|
|
25
132
|
EmptyTitle,
|
|
26
133
|
} from "../components/ui/empty";
|
|
27
|
-
import {
|
|
134
|
+
import {
|
|
135
|
+
Alert,
|
|
136
|
+
AlertAction,
|
|
137
|
+
AlertDescription,
|
|
138
|
+
AlertTitle,
|
|
139
|
+
} from "../components/ui/alert";
|
|
28
140
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
141
|
+
export type DataTableColumnAlign = "left" | "center" | "right";
|
|
142
|
+
type DataTableSortDirection = false | "asc" | "desc";
|
|
143
|
+
|
|
144
|
+
export type DataTableFilterOption = {
|
|
145
|
+
label: string;
|
|
146
|
+
value: string;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type DataTableRangeFilter = {
|
|
150
|
+
min?: string;
|
|
151
|
+
max?: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type DataTableDateRangeFilter = {
|
|
155
|
+
from?: string;
|
|
156
|
+
to?: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Multi-select filter value: an array keeps the matching rows; the object form
|
|
160
|
+
// hides the selected values instead. Server-filtered tables only receive arrays.
|
|
161
|
+
export type DataTableMultiFilterValue =
|
|
162
|
+
string[] | { exclude: true; values: string[] };
|
|
163
|
+
|
|
164
|
+
export type DataTableColumnFilter<TData extends RowData> =
|
|
165
|
+
| {
|
|
166
|
+
placeholder?: string;
|
|
167
|
+
type: "text";
|
|
168
|
+
}
|
|
169
|
+
| {
|
|
170
|
+
options: DataTableFilterOption[];
|
|
171
|
+
type: "multi";
|
|
172
|
+
}
|
|
173
|
+
| {
|
|
174
|
+
max?: number;
|
|
175
|
+
maxPlaceholder?: string;
|
|
176
|
+
min?: number;
|
|
177
|
+
minPlaceholder?: string;
|
|
178
|
+
type: "number-range";
|
|
179
|
+
}
|
|
180
|
+
| {
|
|
181
|
+
type: "date-range";
|
|
182
|
+
}
|
|
183
|
+
| {
|
|
184
|
+
render: (column: Column<TData, unknown>) => React.ReactNode;
|
|
185
|
+
type: "custom";
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type DataTableColumnMeta<TData extends RowData> = {
|
|
189
|
+
align?: DataTableColumnAlign;
|
|
190
|
+
cellClassName?: string;
|
|
191
|
+
filter?: DataTableColumnFilter<TData>;
|
|
192
|
+
headerClassName?: string;
|
|
193
|
+
label?: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
declare module "@tanstack/react-table" {
|
|
197
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
198
|
+
align?: DataTableColumnAlign;
|
|
199
|
+
cellClassName?: string;
|
|
200
|
+
filter?: DataTableColumnFilter<TData>;
|
|
201
|
+
headerClassName?: string;
|
|
202
|
+
label?: string;
|
|
203
|
+
valueType?: TValue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface TableMeta<TData extends RowData> {
|
|
207
|
+
enableExcludeFilters?: boolean;
|
|
208
|
+
rowType?: TData;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Window event that clears a table's search and filters (detail: { storageKey? }). */
|
|
213
|
+
export const DATA_TABLE_RESET_FILTERS_EVENT =
|
|
214
|
+
"majapi-ui:data-table:reset-filters";
|
|
215
|
+
|
|
216
|
+
/** Clears search and filters of every table, or only the one with `storageKey`. */
|
|
217
|
+
export function resetDataTableFilters(storageKey?: string) {
|
|
218
|
+
window.dispatchEvent(
|
|
219
|
+
new CustomEvent(DATA_TABLE_RESET_FILTERS_EVENT, { detail: { storageKey } }),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const dataTableMessages = defineMessages({
|
|
224
|
+
en: {
|
|
225
|
+
search: "Search...",
|
|
226
|
+
resultCount: (count: number) =>
|
|
227
|
+
`${count} ${count === 1 ? "result" : "results"}`,
|
|
228
|
+
rowCount: (count: number) => `${count} ${count === 1 ? "row" : "rows"}`,
|
|
229
|
+
// English needs no plural form here; pt-BR uses count.
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
231
|
+
selected: (rows: string, count: number) => `${rows} selected`,
|
|
232
|
+
clearSelection: "Clear selection",
|
|
233
|
+
clearFilters: "Clear filters",
|
|
234
|
+
open: "Open",
|
|
235
|
+
resizeColumn: (label: string) => `Resize column ${label}`,
|
|
236
|
+
filterActive: ", filter active",
|
|
237
|
+
excludeFilterActive: ", exclusion filter active",
|
|
238
|
+
sortCriterion: (n: number) => `Sort #${n}`,
|
|
239
|
+
sortCriterionTitle: (n: number) => `Sort criterion #${n}`,
|
|
240
|
+
clearAll: "Clear all",
|
|
241
|
+
hide: "Hide",
|
|
242
|
+
toggleSort: (label: string) => `Toggle sorting for ${label}`,
|
|
243
|
+
reorderColumn: (label: string) => `Reorder column ${label}`,
|
|
244
|
+
sort: "Sort",
|
|
245
|
+
removeSort: "Remove sort",
|
|
246
|
+
ascending: "Ascending",
|
|
247
|
+
descending: "Descending",
|
|
248
|
+
filter: "Filter",
|
|
249
|
+
min: "Min.",
|
|
250
|
+
max: "Max.",
|
|
251
|
+
filterBy: (label: string) => `Filter by ${label}`,
|
|
252
|
+
filterMode: "Filter mode",
|
|
253
|
+
show: "Show",
|
|
254
|
+
exclude: "Exclude",
|
|
255
|
+
valuesToExclude: "Values to exclude",
|
|
256
|
+
valuesToShow: "Values to show",
|
|
257
|
+
customizeColumns: "Customize columns",
|
|
258
|
+
columnsTooltip: "Show, hide and reset columns",
|
|
259
|
+
columns: "Columns",
|
|
260
|
+
resetColumns: "Reset columns",
|
|
261
|
+
rowsPerPage: "Rows",
|
|
262
|
+
page: (page: number, total: number) => `Page ${page} of ${total}`,
|
|
263
|
+
ofSummary: (part: string, total: string) => `${part} of ${total}.`,
|
|
264
|
+
firstPage: "First page",
|
|
265
|
+
previousPage: "Previous page",
|
|
266
|
+
nextPage: "Next page",
|
|
267
|
+
lastPage: "Last page",
|
|
268
|
+
},
|
|
269
|
+
"pt-BR": {
|
|
270
|
+
search: "Buscar...",
|
|
271
|
+
resultCount: (count: number) =>
|
|
272
|
+
`${count} resultado${count === 1 ? "" : "s"}`,
|
|
273
|
+
rowCount: (count: number) => `${count} registro${count === 1 ? "" : "s"}`,
|
|
274
|
+
selected: (rows: string, count: number) =>
|
|
275
|
+
`${rows} selecionado${count === 1 ? "" : "s"}`,
|
|
276
|
+
clearSelection: "Limpar seleção",
|
|
277
|
+
clearFilters: "Limpar filtros",
|
|
278
|
+
open: "Abrir",
|
|
279
|
+
resizeColumn: (label: string) => `Redimensionar coluna ${label}`,
|
|
280
|
+
filterActive: ", filtro ativo",
|
|
281
|
+
excludeFilterActive: ", filtro de exclusão ativo",
|
|
282
|
+
sortCriterion: (n: number) => `${n}º critério`,
|
|
283
|
+
sortCriterionTitle: (n: number) => `${n}º critério de ordenação`,
|
|
284
|
+
clearAll: "Limpar tudo",
|
|
285
|
+
hide: "Ocultar",
|
|
286
|
+
toggleSort: (label: string) => `Alternar ordenação da coluna ${label}`,
|
|
287
|
+
reorderColumn: (label: string) => `Reordenar coluna ${label}`,
|
|
288
|
+
sort: "Ordenar",
|
|
289
|
+
removeSort: "Remover ordem",
|
|
290
|
+
ascending: "Crescente",
|
|
291
|
+
descending: "Decrescente",
|
|
292
|
+
filter: "Filtrar",
|
|
293
|
+
min: "Mín.",
|
|
294
|
+
max: "Máx.",
|
|
295
|
+
filterBy: (label: string) => `Filtrar por ${label}`,
|
|
296
|
+
filterMode: "Modo do filtro",
|
|
297
|
+
show: "Mostrar",
|
|
298
|
+
exclude: "Excluir",
|
|
299
|
+
valuesToExclude: "Valores a excluir",
|
|
300
|
+
valuesToShow: "Valores a mostrar",
|
|
301
|
+
customizeColumns: "Personalizar colunas",
|
|
302
|
+
columnsTooltip: "Mostrar, ocultar e restaurar colunas",
|
|
303
|
+
columns: "Colunas",
|
|
304
|
+
resetColumns: "Restaurar colunas",
|
|
305
|
+
rowsPerPage: "Linhas",
|
|
306
|
+
page: (page: number, total: number) => `Página ${page} de ${total}`,
|
|
307
|
+
ofSummary: (part: string, total: string) => `${part} de ${total}.`,
|
|
308
|
+
firstPage: "Primeira página",
|
|
309
|
+
previousPage: "Página anterior",
|
|
310
|
+
nextPage: "Próxima página",
|
|
311
|
+
lastPage: "Última página",
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
type StoredDataTableSettings = {
|
|
316
|
+
columnFilters?: ColumnFiltersState;
|
|
317
|
+
columnOrder?: ColumnOrderState;
|
|
318
|
+
columnSizing?: ColumnSizingState;
|
|
319
|
+
columnVisibility?: VisibilityState;
|
|
320
|
+
globalFilter?: string;
|
|
45
321
|
pageSize?: number;
|
|
322
|
+
sorting?: SortingState;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export type DataTableProps<TData extends RowData> = {
|
|
326
|
+
activeRowId?: string | null;
|
|
327
|
+
columns: ColumnDef<TData, unknown>[];
|
|
328
|
+
data: TData[];
|
|
329
|
+
/** Text when there are no rows; defaults to a translated message. */
|
|
330
|
+
emptyMessage?: string;
|
|
331
|
+
/** Rich empty state (e.g. with a create action), shown when nothing is filtered. */
|
|
332
|
+
emptyState?: React.ReactNode;
|
|
333
|
+
/**
|
|
334
|
+
* Fills the parent's height and scrolls rows inside the table (sticky
|
|
335
|
+
* header, toolbar and pagination stay visible). The parent must be a flex
|
|
336
|
+
* column with a bounded height, e.g. `AppShell fillHeight`.
|
|
337
|
+
*/
|
|
338
|
+
fillHeight?: boolean;
|
|
339
|
+
/** Shows skeleton rows instead of data. */
|
|
340
|
+
loading?: boolean;
|
|
341
|
+
/** Load failure. With rows it shows as stale data; without, replaces the table. */
|
|
342
|
+
error?: string;
|
|
343
|
+
onRetry?: () => void;
|
|
344
|
+
/** Non-blocking notice, e.g. partial coverage. */
|
|
345
|
+
warning?: string;
|
|
346
|
+
/** Data can't be shown at all, e.g. missing permission. */
|
|
347
|
+
unavailable?: string;
|
|
348
|
+
enablePersistence?: boolean;
|
|
349
|
+
enableRowSelection?: boolean;
|
|
350
|
+
// Offers "Excluir" in multi-select column filters. Defaults to off for
|
|
351
|
+
// server-backed tables (onStateChange), whose APIs only read include arrays.
|
|
352
|
+
enableExcludeFilters?: boolean;
|
|
353
|
+
filterFns?: Record<string, FilterFn<TData>>;
|
|
354
|
+
formatResultCount?: (count: number) => string;
|
|
355
|
+
formatRowCount?: (count: number) => string;
|
|
356
|
+
formatSelectedRowCount?: (count: number) => string;
|
|
357
|
+
getRowId: (row: TData) => string;
|
|
358
|
+
globalFilterFn?: FilterFn<TData>;
|
|
359
|
+
initialColumnFilters?: ColumnFiltersState;
|
|
360
|
+
/** Initial search text. */
|
|
361
|
+
initialGlobalFilter?: string;
|
|
362
|
+
/** Makes rows clickable (and Enter/Space activatable), e.g. to open details. */
|
|
363
|
+
onRowClick?: (row: TData) => void;
|
|
364
|
+
/** Accessible name for a clickable row; defaults to its id. */
|
|
365
|
+
getRowLabel?: (row: TData) => string;
|
|
366
|
+
initialColumnOrder?: ColumnOrderState;
|
|
367
|
+
initialColumnVisibility?: VisibilityState;
|
|
368
|
+
initialPageSize?: number;
|
|
369
|
+
initialSorting?: SortingState;
|
|
370
|
+
lockedEndColumnIds?: string[];
|
|
371
|
+
lockedStartColumnIds?: string[];
|
|
372
|
+
manualFiltering?: boolean;
|
|
373
|
+
manualPagination?: boolean;
|
|
374
|
+
manualSorting?: boolean;
|
|
375
|
+
onStateChange?: (state: DataTableStateSnapshot) => void;
|
|
376
|
+
pageCount?: number;
|
|
377
|
+
pageSizeOptions?: number[];
|
|
378
|
+
rowCount?: number;
|
|
379
|
+
rowSelectionActions?: (table: TanstackTable<TData>) => React.ReactNode;
|
|
380
|
+
searchPlaceholder?: string;
|
|
381
|
+
/** Persists sorting, filters, columns and page size in localStorage under this key. */
|
|
382
|
+
storageKey?: string;
|
|
383
|
+
toolbarBelow?: (table: TanstackTable<TData>) => React.ReactNode;
|
|
384
|
+
toolbarEnd?: (table: TanstackTable<TData>) => React.ReactNode;
|
|
385
|
+
toolbarStart?: (table: TanstackTable<TData>) => React.ReactNode;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export type DataTableStateSnapshot = {
|
|
389
|
+
columnFilters: ColumnFiltersState;
|
|
390
|
+
globalFilter: string;
|
|
391
|
+
pagination: PaginationState;
|
|
392
|
+
sorting: SortingState;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export type DataTableFilterTab = {
|
|
396
|
+
count?: number;
|
|
397
|
+
label: string;
|
|
398
|
+
value: string;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
type DataTableObjectFilter = DataTableRangeFilter & DataTableDateRangeFilter;
|
|
402
|
+
|
|
403
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
404
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function readStringProperty(value: unknown, key: string) {
|
|
408
|
+
if (!isRecord(value)) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const property = value[key];
|
|
413
|
+
return typeof property === "string" ? property : undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function readStringArrayFilter(value: unknown) {
|
|
417
|
+
if (!Array.isArray(value)) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return value.filter((item): item is string => typeof item === "string");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function readMultiFilterValue(value: unknown): {
|
|
425
|
+
exclude: boolean;
|
|
426
|
+
values: string[];
|
|
427
|
+
} {
|
|
428
|
+
if (isRecord(value) && value.exclude === true) {
|
|
429
|
+
return { exclude: true, values: readStringArrayFilter(value.values) };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { exclude: false, values: readStringArrayFilter(value) };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function toMultiFilterValue(
|
|
436
|
+
exclude: boolean,
|
|
437
|
+
values: string[],
|
|
438
|
+
): DataTableMultiFilterValue | undefined {
|
|
439
|
+
if (values.length === 0) {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return exclude ? { exclude: true, values } : values;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function isString(value: unknown): value is string {
|
|
447
|
+
return typeof value === "string";
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function readRangeFilterValue(value: unknown): DataTableRangeFilter {
|
|
451
|
+
return {
|
|
452
|
+
max: readStringProperty(value, "max"),
|
|
453
|
+
min: readStringProperty(value, "min"),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function readDateRangeFilterValue(value: unknown): DataTableDateRangeFilter {
|
|
458
|
+
return {
|
|
459
|
+
from: readStringProperty(value, "from"),
|
|
460
|
+
to: readStringProperty(value, "to"),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function readObjectFilterValue(value: unknown): Partial<DataTableObjectFilter> {
|
|
465
|
+
return {
|
|
466
|
+
...readRangeFilterValue(value),
|
|
467
|
+
...readDateRangeFilterValue(value),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function createArrayIncludesSomeFilter<
|
|
472
|
+
TData extends RowData,
|
|
473
|
+
>(): FilterFn<TData> {
|
|
474
|
+
return (row, columnId, filterValue) => {
|
|
475
|
+
const { exclude, values } = readMultiFilterValue(filterValue);
|
|
476
|
+
|
|
477
|
+
if (values.length === 0) {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return values.includes(row.getValue(columnId)) !== exclude;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function createNumberInRangeFilter<
|
|
486
|
+
TData extends RowData,
|
|
487
|
+
>(): FilterFn<TData> {
|
|
488
|
+
return (row, columnId, filterValue) => {
|
|
489
|
+
const range = readRangeFilterValue(filterValue);
|
|
490
|
+
const min = range?.min ? Number(range.min) : null;
|
|
491
|
+
const max = range?.max ? Number(range.max) : null;
|
|
492
|
+
const value = Number(row.getValue(columnId));
|
|
493
|
+
|
|
494
|
+
if (min !== null && Number.isFinite(min) && value < min) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (max !== null && Number.isFinite(max) && value > max) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return true;
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function createDateInRangeFilter<
|
|
507
|
+
TData extends RowData,
|
|
508
|
+
>(): FilterFn<TData> {
|
|
509
|
+
return (row, columnId, filterValue) => {
|
|
510
|
+
const range = readDateRangeFilterValue(filterValue);
|
|
511
|
+
const value = new Date(row.getValue<string>(columnId)).getTime();
|
|
512
|
+
const from = range?.from
|
|
513
|
+
? new Date(`${range.from}T00:00:00`).getTime()
|
|
514
|
+
: null;
|
|
515
|
+
const to = range?.to ? new Date(`${range.to}T23:59:59`).getTime() : null;
|
|
516
|
+
|
|
517
|
+
if (from !== null && Number.isFinite(from) && value < from) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (to !== null && Number.isFinite(to) && value > to) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return true;
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function getSingleArrayFilterValue(
|
|
530
|
+
filterValue: unknown,
|
|
531
|
+
): "all" | "custom" | string {
|
|
532
|
+
const { exclude, values } = readMultiFilterValue(filterValue);
|
|
533
|
+
|
|
534
|
+
if (exclude && values.length > 0) {
|
|
535
|
+
return "custom";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (values.length === 0) {
|
|
539
|
+
return "all";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (values.length === 1) {
|
|
543
|
+
return values[0] ?? "all";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return "custom";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function DataTableColumnFilterTabs<TData extends RowData>({
|
|
550
|
+
className,
|
|
551
|
+
columnId,
|
|
552
|
+
options,
|
|
553
|
+
table,
|
|
554
|
+
}: {
|
|
555
|
+
className?: string;
|
|
556
|
+
columnId: string;
|
|
557
|
+
options: DataTableFilterTab[];
|
|
558
|
+
table: TanstackTable<TData>;
|
|
559
|
+
}) {
|
|
560
|
+
const column = table.getColumn(columnId);
|
|
561
|
+
const value = getSingleArrayFilterValue(column?.getFilterValue());
|
|
562
|
+
|
|
563
|
+
const handleValueChange = (nextValue: string) => {
|
|
564
|
+
column?.setFilterValue(nextValue === "all" ? undefined : [nextValue]);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
return (
|
|
568
|
+
<FilterTabs
|
|
569
|
+
className={className}
|
|
570
|
+
onValueChange={handleValueChange}
|
|
571
|
+
options={options}
|
|
572
|
+
value={value}
|
|
573
|
+
/>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function createDataTableFilterTabOptions<TItem>(
|
|
578
|
+
items: TItem[],
|
|
579
|
+
filters: Array<Pick<DataTableFilterTab, "label" | "value">>,
|
|
580
|
+
getItemValue: (item: TItem) => string,
|
|
581
|
+
) {
|
|
582
|
+
return filters.map((filter) => ({
|
|
583
|
+
...filter,
|
|
584
|
+
count:
|
|
585
|
+
filter.value === "all"
|
|
586
|
+
? undefined
|
|
587
|
+
: items.filter((item) => getItemValue(item) === filter.value).length,
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function getColumnId<TData extends RowData>(
|
|
592
|
+
column: ColumnDef<TData, unknown>,
|
|
593
|
+
): string | null {
|
|
594
|
+
if ("id" in column && column.id) {
|
|
595
|
+
return column.id;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if ("accessorKey" in column && typeof column.accessorKey === "string") {
|
|
599
|
+
return column.accessorKey;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function normalizeColumnOrder(
|
|
606
|
+
columnOrder: ColumnOrderState | undefined,
|
|
607
|
+
defaultColumnOrder: ColumnOrderState,
|
|
608
|
+
lockedStartColumnIds: string[],
|
|
609
|
+
lockedEndColumnIds: string[],
|
|
610
|
+
) {
|
|
611
|
+
const validColumns = new Set(defaultColumnOrder);
|
|
612
|
+
const lockedStart = lockedStartColumnIds.filter((columnId) =>
|
|
613
|
+
validColumns.has(columnId),
|
|
614
|
+
);
|
|
615
|
+
const lockedEnd = lockedEndColumnIds.filter((columnId) =>
|
|
616
|
+
validColumns.has(columnId),
|
|
617
|
+
);
|
|
618
|
+
const lockedColumns = new Set([...lockedStart, ...lockedEnd]);
|
|
619
|
+
const movableColumns = (columnOrder ?? []).filter(
|
|
620
|
+
(columnId) => validColumns.has(columnId) && !lockedColumns.has(columnId),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
for (const columnId of defaultColumnOrder) {
|
|
624
|
+
if (!lockedColumns.has(columnId) && !movableColumns.includes(columnId)) {
|
|
625
|
+
movableColumns.push(columnId);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return [...lockedStart, ...movableColumns, ...lockedEnd];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function areColumnOrdersEqual(left: ColumnOrderState, right: ColumnOrderState) {
|
|
633
|
+
return (
|
|
634
|
+
left.length === right.length &&
|
|
635
|
+
left.every((columnId, index) => columnId === right[index])
|
|
636
|
+
);
|
|
46
637
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
638
|
+
|
|
639
|
+
function getColumnMeta<TData extends RowData>(column: Column<TData, unknown>) {
|
|
640
|
+
return column.columnDef.meta;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function getColumnLabel<TData extends RowData>(column: Column<TData, unknown>) {
|
|
644
|
+
const header = column.columnDef.header;
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
getColumnMeta(column)?.label ??
|
|
648
|
+
(typeof header === "string" ? header : column.id)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function getColumnAlign<TData extends RowData>(column: Column<TData, unknown>) {
|
|
653
|
+
return getColumnMeta(column)?.align ?? "left";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function getColumnAlignClass<TData extends RowData>(
|
|
657
|
+
column: Column<TData, unknown>,
|
|
658
|
+
) {
|
|
659
|
+
const align = getColumnAlign(column);
|
|
660
|
+
|
|
661
|
+
return cn(
|
|
662
|
+
align === "center" && "text-center",
|
|
663
|
+
align === "right" && "text-right",
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function getColumnAriaSort(
|
|
668
|
+
isSorted: DataTableSortDirection,
|
|
669
|
+
): "ascending" | "descending" | undefined {
|
|
670
|
+
if (!isSorted) {
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return isSorted === "asc" ? "ascending" : "descending";
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function getHeaderControlVisibilityClass(isVisible: boolean) {
|
|
678
|
+
if (isVisible) {
|
|
679
|
+
return "pointer-events-auto opacity-100";
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return "pointer-events-none opacity-0 group-focus-within/header:pointer-events-auto group-focus-within/header:opacity-100 group-hover/header:pointer-events-auto group-hover/header:opacity-100";
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function hasRangeFilter(value: unknown) {
|
|
686
|
+
const range = readObjectFilterValue(value);
|
|
687
|
+
return Boolean(range.min || range.max || range.from || range.to);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function isFilterValueActive(value: unknown) {
|
|
691
|
+
if (Array.isArray(value)) {
|
|
692
|
+
return value.length > 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (isRecord(value) && value.exclude === true) {
|
|
696
|
+
return readMultiFilterValue(value).values.length > 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (typeof value === "object") {
|
|
700
|
+
return hasRangeFilter(value);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return Boolean(value);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function setObjectFilterValue<TData extends RowData>(
|
|
707
|
+
column: Column<TData, unknown>,
|
|
708
|
+
key: keyof DataTableObjectFilter,
|
|
709
|
+
value: string,
|
|
710
|
+
) {
|
|
711
|
+
const next = {
|
|
712
|
+
...readObjectFilterValue(column.getFilterValue()),
|
|
713
|
+
[key]: value,
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
if (!hasRangeFilter(next)) {
|
|
717
|
+
column.setFilterValue(undefined);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
column.setFilterValue(next);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function setColumnSorting<TData extends RowData>(
|
|
725
|
+
table: TanstackTable<TData>,
|
|
726
|
+
columnId: string,
|
|
727
|
+
desc: boolean,
|
|
728
|
+
) {
|
|
729
|
+
table.setSorting((current) => {
|
|
730
|
+
const next = [
|
|
731
|
+
...current.filter((sort) => sort.id !== columnId),
|
|
732
|
+
{ id: columnId, desc },
|
|
733
|
+
];
|
|
734
|
+
|
|
735
|
+
return next.slice(-5);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function clearColumnSorting<TData extends RowData>(
|
|
740
|
+
table: TanstackTable<TData>,
|
|
741
|
+
columnId: string,
|
|
742
|
+
) {
|
|
743
|
+
table.setSorting((current) => current.filter((sort) => sort.id !== columnId));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function toggleColumnSorting<TData extends RowData>(
|
|
747
|
+
table: TanstackTable<TData>,
|
|
748
|
+
columnId: string,
|
|
749
|
+
) {
|
|
750
|
+
table.setSorting((current) => {
|
|
751
|
+
const currentSort = current.find((sort) => sort.id === columnId);
|
|
752
|
+
const next = current.filter((sort) => sort.id !== columnId);
|
|
753
|
+
|
|
754
|
+
if (!currentSort) {
|
|
755
|
+
return [...next, { id: columnId, desc: false }].slice(-5);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (!currentSort.desc) {
|
|
759
|
+
return [...next, { id: columnId, desc: true }].slice(-5);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return next;
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function isColumnOrderState(value: unknown): value is ColumnOrderState {
|
|
767
|
+
return (
|
|
768
|
+
Array.isArray(value) &&
|
|
769
|
+
value.every((columnId) => typeof columnId === "string")
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function isColumnFiltersState(value: unknown): value is ColumnFiltersState {
|
|
774
|
+
return (
|
|
775
|
+
Array.isArray(value) &&
|
|
776
|
+
value.every((filter) => isRecord(filter) && typeof filter.id === "string")
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function isVisibilityState(value: unknown): value is VisibilityState {
|
|
781
|
+
return (
|
|
782
|
+
isRecord(value) &&
|
|
783
|
+
Object.values(value).every((visible) => typeof visible === "boolean")
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function isSortingState(value: unknown): value is SortingState {
|
|
788
|
+
return (
|
|
789
|
+
Array.isArray(value) &&
|
|
790
|
+
value.every(
|
|
791
|
+
(sort) =>
|
|
792
|
+
isRecord(sort) &&
|
|
793
|
+
typeof sort.id === "string" &&
|
|
794
|
+
typeof sort.desc === "boolean",
|
|
795
|
+
)
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function parseStoredDataTableSettings(
|
|
800
|
+
value: string,
|
|
801
|
+
): StoredDataTableSettings | null {
|
|
802
|
+
const parsed = JSON.parse(value);
|
|
803
|
+
|
|
804
|
+
if (!isRecord(parsed)) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const settings: StoredDataTableSettings = {};
|
|
809
|
+
|
|
810
|
+
if (isColumnFiltersState(parsed.columnFilters)) {
|
|
811
|
+
settings.columnFilters = parsed.columnFilters;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (isColumnOrderState(parsed.columnOrder)) {
|
|
815
|
+
settings.columnOrder = parsed.columnOrder;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (isVisibilityState(parsed.columnVisibility)) {
|
|
819
|
+
settings.columnVisibility = parsed.columnVisibility;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (typeof parsed.globalFilter === "string") {
|
|
823
|
+
settings.globalFilter = parsed.globalFilter;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (typeof parsed.pageSize === "number" && Number.isFinite(parsed.pageSize)) {
|
|
827
|
+
settings.pageSize = parsed.pageSize;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (isSortingState(parsed.sorting)) {
|
|
831
|
+
settings.sorting = parsed.sorting;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return settings;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function getStoredDataTableSettings(storageKey: string) {
|
|
838
|
+
if (typeof window === "undefined") {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
const value = window.localStorage.getItem(storageKey);
|
|
844
|
+
return value ? parseStoredDataTableSettings(value) : null;
|
|
845
|
+
} catch {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function pickKnownColumnEntries<TValue>(
|
|
851
|
+
record: Record<string, TValue> | undefined,
|
|
852
|
+
columnIds: string[],
|
|
853
|
+
) {
|
|
854
|
+
if (!record) {
|
|
855
|
+
return undefined;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return Object.fromEntries(
|
|
859
|
+
Object.entries(record).filter(([columnId]) => columnIds.includes(columnId)),
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function retainKnownColumnSettings(
|
|
864
|
+
settings: StoredDataTableSettings | null,
|
|
865
|
+
columnIds: string[],
|
|
866
|
+
): StoredDataTableSettings | null {
|
|
867
|
+
if (!settings) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const sorting = settings.sorting?.filter((sort) =>
|
|
872
|
+
columnIds.includes(sort.id),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
...settings,
|
|
877
|
+
columnFilters: settings.columnFilters?.filter((filter) =>
|
|
878
|
+
columnIds.includes(filter.id),
|
|
879
|
+
),
|
|
880
|
+
columnSizing: pickKnownColumnEntries(settings.columnSizing, columnIds),
|
|
881
|
+
columnVisibility: pickKnownColumnEntries(
|
|
882
|
+
settings.columnVisibility,
|
|
883
|
+
columnIds,
|
|
884
|
+
),
|
|
885
|
+
sorting: sorting?.length ? sorting : undefined,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function getResetFiltersStorageKey(event: Event) {
|
|
890
|
+
if (!(event instanceof CustomEvent)) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const detail = event.detail;
|
|
895
|
+
if (!isRecord(detail)) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const storageKey = detail.storageKey;
|
|
900
|
+
return typeof storageKey === "string" ? storageKey : null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function isLockedColumn(
|
|
904
|
+
columnId: string,
|
|
905
|
+
lockedStartColumnIds: string[],
|
|
906
|
+
lockedEndColumnIds: string[],
|
|
907
|
+
) {
|
|
908
|
+
return (
|
|
909
|
+
lockedStartColumnIds.includes(columnId) ||
|
|
910
|
+
lockedEndColumnIds.includes(columnId)
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function getStorageSafeId(storageKey: string) {
|
|
915
|
+
return storageKey.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function getOptionalColumnOrderKey(columnOrder?: ColumnOrderState) {
|
|
919
|
+
return columnOrder?.join("|") ?? "";
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function getColumnFiltersKey(columnFilters: ColumnFiltersState) {
|
|
923
|
+
return JSON.stringify(columnFilters);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function getVisibilityStateKey(visibility: VisibilityState) {
|
|
927
|
+
const entries = Object.entries(visibility).sort(([left], [right]) =>
|
|
928
|
+
left.localeCompare(right),
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
return JSON.stringify(entries);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function getSortingKey(sorting: SortingState) {
|
|
935
|
+
return JSON.stringify(sorting);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function useStableValueByKey<TValue>(value: TValue, key: string) {
|
|
939
|
+
const [stored, setStored] = React.useState({ key, value });
|
|
940
|
+
|
|
941
|
+
React.useEffect(() => {
|
|
942
|
+
setStored((current) => (current.key === key ? current : { key, value }));
|
|
943
|
+
}, [key, value]);
|
|
944
|
+
|
|
945
|
+
return stored.value;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function useDataTableInstance<TData extends RowData>(
|
|
949
|
+
options: TableOptions<TData>,
|
|
950
|
+
) {
|
|
951
|
+
const resolvedOptions: TableOptionsResolved<TData> = {
|
|
952
|
+
onStateChange: () => undefined,
|
|
953
|
+
renderFallbackValue: null,
|
|
954
|
+
state: {},
|
|
955
|
+
...options,
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const [table] = React.useState(() => createTable<TData>(resolvedOptions));
|
|
959
|
+
const [state, setState] = React.useState(() => table.initialState);
|
|
960
|
+
|
|
961
|
+
table.setOptions((previous) => ({
|
|
962
|
+
...previous,
|
|
963
|
+
...options,
|
|
964
|
+
onStateChange: (updater) => {
|
|
965
|
+
setState(updater);
|
|
966
|
+
options.onStateChange?.(updater);
|
|
967
|
+
},
|
|
968
|
+
state: {
|
|
969
|
+
...state,
|
|
970
|
+
...options.state,
|
|
971
|
+
},
|
|
972
|
+
}));
|
|
973
|
+
|
|
974
|
+
return table;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const emptyColumnFilters: ColumnFiltersState = [];
|
|
978
|
+
const emptyColumnSizing: ColumnSizingState = {};
|
|
979
|
+
const emptyColumnVisibility: VisibilityState = {};
|
|
980
|
+
const emptySorting: SortingState = [];
|
|
981
|
+
const emptyLockedColumnIds: string[] = [];
|
|
982
|
+
const defaultPageSizeOptions = [10, 20, 30, 50, 100];
|
|
983
|
+
|
|
984
|
+
function resolvePageSize(
|
|
985
|
+
pageSize: number | undefined,
|
|
986
|
+
fallbackPageSize: number,
|
|
987
|
+
pageSizeOptions: number[],
|
|
988
|
+
) {
|
|
989
|
+
if (typeof pageSize === "number" && pageSizeOptions.includes(pageSize)) {
|
|
990
|
+
return pageSize;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (pageSizeOptions.includes(fallbackPageSize)) {
|
|
994
|
+
return fallbackPageSize;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return pageSizeOptions[0] ?? fallbackPageSize;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export function DataTable<TData extends RowData>(props: DataTableProps<TData>) {
|
|
1001
|
+
const {
|
|
1002
|
+
activeRowId,
|
|
1003
|
+
columns,
|
|
1004
|
+
data,
|
|
1005
|
+
emptyMessage,
|
|
1006
|
+
emptyState,
|
|
1007
|
+
fillHeight = false,
|
|
1008
|
+
loading = false,
|
|
1009
|
+
error,
|
|
1010
|
+
onRetry,
|
|
1011
|
+
warning,
|
|
1012
|
+
unavailable,
|
|
1013
|
+
enablePersistence: enablePersistenceProp,
|
|
1014
|
+
enableRowSelection = false,
|
|
1015
|
+
filterFns,
|
|
1016
|
+
formatResultCount: formatResultCountProp,
|
|
1017
|
+
formatRowCount: formatRowCountProp,
|
|
1018
|
+
formatSelectedRowCount,
|
|
1019
|
+
getRowId,
|
|
1020
|
+
globalFilterFn,
|
|
1021
|
+
initialColumnFilters = emptyColumnFilters,
|
|
1022
|
+
initialGlobalFilter = "",
|
|
1023
|
+
onRowClick,
|
|
1024
|
+
getRowLabel,
|
|
1025
|
+
initialColumnOrder,
|
|
1026
|
+
initialColumnVisibility = emptyColumnVisibility,
|
|
1027
|
+
initialPageSize = 10,
|
|
1028
|
+
initialSorting = emptySorting,
|
|
1029
|
+
lockedEndColumnIds = emptyLockedColumnIds,
|
|
1030
|
+
lockedStartColumnIds = emptyLockedColumnIds,
|
|
1031
|
+
manualFiltering = false,
|
|
1032
|
+
enableExcludeFilters,
|
|
1033
|
+
manualPagination = false,
|
|
1034
|
+
manualSorting = false,
|
|
1035
|
+
onStateChange,
|
|
1036
|
+
pageCount,
|
|
1037
|
+
pageSizeOptions = defaultPageSizeOptions,
|
|
1038
|
+
rowCount,
|
|
1039
|
+
rowSelectionActions,
|
|
1040
|
+
searchPlaceholder,
|
|
1041
|
+
storageKey: storageKeyProp,
|
|
1042
|
+
toolbarBelow,
|
|
1043
|
+
toolbarEnd,
|
|
1044
|
+
toolbarStart,
|
|
1045
|
+
} = props;
|
|
1046
|
+
const t = useMessages(dataTableMessages);
|
|
1047
|
+
const ui = useUiMessages();
|
|
1048
|
+
// Persistence needs a key; without one the table keeps state in memory only.
|
|
1049
|
+
const storageKey = storageKeyProp ?? "";
|
|
1050
|
+
const enablePersistence =
|
|
1051
|
+
Boolean(storageKeyProp) && enablePersistenceProp !== false;
|
|
1052
|
+
const formatResultCount = formatResultCountProp ?? t.resultCount;
|
|
1053
|
+
const formatRowCount = formatRowCountProp ?? t.rowCount;
|
|
1054
|
+
|
|
1055
|
+
const defaultColumnOrder = React.useMemo(
|
|
1056
|
+
() => columns.map(getColumnId).filter(isString),
|
|
1057
|
+
[columns],
|
|
1058
|
+
);
|
|
1059
|
+
const defaultColumnOrderKey = defaultColumnOrder.join("|");
|
|
1060
|
+
const pageSizeOptionsKey = pageSizeOptions.join("|");
|
|
1061
|
+
const lockedStartColumnKey = lockedStartColumnIds.join("|");
|
|
1062
|
+
const lockedEndColumnKey = lockedEndColumnIds.join("|");
|
|
1063
|
+
const stablePageSizeOptions = useStableValueByKey(
|
|
1064
|
+
pageSizeOptions,
|
|
1065
|
+
pageSizeOptionsKey,
|
|
1066
|
+
);
|
|
1067
|
+
const stableDefaultColumnOrder = useStableValueByKey(
|
|
1068
|
+
defaultColumnOrder,
|
|
1069
|
+
defaultColumnOrderKey,
|
|
1070
|
+
);
|
|
1071
|
+
const stableInitialColumnFilters = useStableValueByKey(
|
|
1072
|
+
initialColumnFilters,
|
|
1073
|
+
getColumnFiltersKey(initialColumnFilters),
|
|
1074
|
+
);
|
|
1075
|
+
const stableInitialColumnOrder = useStableValueByKey(
|
|
1076
|
+
initialColumnOrder,
|
|
1077
|
+
getOptionalColumnOrderKey(initialColumnOrder),
|
|
1078
|
+
);
|
|
1079
|
+
const stableInitialColumnVisibility = useStableValueByKey(
|
|
1080
|
+
initialColumnVisibility,
|
|
1081
|
+
getVisibilityStateKey(initialColumnVisibility),
|
|
1082
|
+
);
|
|
1083
|
+
const stableInitialSorting = useStableValueByKey(
|
|
1084
|
+
initialSorting,
|
|
1085
|
+
getSortingKey(initialSorting),
|
|
1086
|
+
);
|
|
1087
|
+
const stableLockedStartColumnIds = useStableValueByKey(
|
|
1088
|
+
lockedStartColumnIds,
|
|
1089
|
+
lockedStartColumnKey,
|
|
1090
|
+
);
|
|
1091
|
+
const stableLockedEndColumnIds = useStableValueByKey(
|
|
1092
|
+
lockedEndColumnIds,
|
|
1093
|
+
lockedEndColumnKey,
|
|
1094
|
+
);
|
|
1095
|
+
const [preferencesReady, setPreferencesReady] =
|
|
1096
|
+
React.useState(!enablePersistence);
|
|
1097
|
+
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
|
|
1098
|
+
const [columnVisibility, setColumnVisibility] =
|
|
1099
|
+
React.useState<VisibilityState>(stableInitialColumnVisibility);
|
|
1100
|
+
const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>(() =>
|
|
1101
|
+
normalizeColumnOrder(
|
|
1102
|
+
stableInitialColumnOrder ?? stableDefaultColumnOrder,
|
|
1103
|
+
stableDefaultColumnOrder,
|
|
1104
|
+
stableLockedStartColumnIds,
|
|
1105
|
+
stableLockedEndColumnIds,
|
|
1106
|
+
),
|
|
1107
|
+
);
|
|
1108
|
+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
|
1109
|
+
stableInitialColumnFilters,
|
|
1110
|
+
);
|
|
1111
|
+
const [columnSizing, setColumnSizing] =
|
|
1112
|
+
React.useState<ColumnSizingState>(emptyColumnSizing);
|
|
1113
|
+
const [sorting, setSorting] =
|
|
1114
|
+
React.useState<SortingState>(stableInitialSorting);
|
|
1115
|
+
const [globalFilter, setGlobalFilter] = React.useState(initialGlobalFilter);
|
|
1116
|
+
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
1117
|
+
pageIndex: 0,
|
|
1118
|
+
pageSize: resolvePageSize(
|
|
1119
|
+
initialPageSize,
|
|
1120
|
+
initialPageSize,
|
|
1121
|
+
stablePageSizeOptions,
|
|
1122
|
+
),
|
|
1123
|
+
});
|
|
1124
|
+
const resetPaginationPage = React.useCallback(() => {
|
|
1125
|
+
setPagination((current) =>
|
|
1126
|
+
current.pageIndex === 0 ? current : { ...current, pageIndex: 0 },
|
|
1127
|
+
);
|
|
1128
|
+
}, []);
|
|
1129
|
+
const handleColumnFiltersChange = React.useCallback<
|
|
1130
|
+
OnChangeFn<ColumnFiltersState>
|
|
1131
|
+
>(
|
|
1132
|
+
(updater) => {
|
|
1133
|
+
setColumnFilters(updater);
|
|
1134
|
+
resetPaginationPage();
|
|
1135
|
+
},
|
|
1136
|
+
[resetPaginationPage],
|
|
1137
|
+
);
|
|
1138
|
+
const handleGlobalFilterChange = React.useCallback<OnChangeFn<string>>(
|
|
1139
|
+
(updater) => {
|
|
1140
|
+
setGlobalFilter(updater);
|
|
1141
|
+
resetPaginationPage();
|
|
1142
|
+
},
|
|
1143
|
+
[resetPaginationPage],
|
|
1144
|
+
);
|
|
1145
|
+
const handleSortingChange = React.useCallback<OnChangeFn<SortingState>>(
|
|
1146
|
+
(updater) => {
|
|
1147
|
+
setSorting(updater);
|
|
1148
|
+
resetPaginationPage();
|
|
1149
|
+
},
|
|
1150
|
+
[resetPaginationPage],
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
const dndContextId = React.useId();
|
|
1154
|
+
const sensors = useSensors(
|
|
1155
|
+
useSensor(MouseSensor, {
|
|
1156
|
+
activationConstraint: {
|
|
1157
|
+
distance: 6,
|
|
1158
|
+
},
|
|
1159
|
+
}),
|
|
1160
|
+
useSensor(TouchSensor, {
|
|
1161
|
+
activationConstraint: {
|
|
1162
|
+
delay: 150,
|
|
1163
|
+
tolerance: 6,
|
|
1164
|
+
},
|
|
1165
|
+
}),
|
|
1166
|
+
useSensor(KeyboardSensor),
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
React.useEffect(() => {
|
|
1170
|
+
if (!enablePersistence) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const settings = retainKnownColumnSettings(
|
|
1175
|
+
getStoredDataTableSettings(storageKey),
|
|
1176
|
+
stableDefaultColumnOrder,
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
setColumnVisibility(
|
|
1180
|
+
settings?.columnVisibility ?? stableInitialColumnVisibility,
|
|
1181
|
+
);
|
|
1182
|
+
setColumnOrder(
|
|
1183
|
+
normalizeColumnOrder(
|
|
1184
|
+
settings?.columnOrder ??
|
|
1185
|
+
stableInitialColumnOrder ??
|
|
1186
|
+
stableDefaultColumnOrder,
|
|
1187
|
+
stableDefaultColumnOrder,
|
|
1188
|
+
stableLockedStartColumnIds,
|
|
1189
|
+
stableLockedEndColumnIds,
|
|
1190
|
+
),
|
|
1191
|
+
);
|
|
1192
|
+
setColumnFilters(settings?.columnFilters ?? stableInitialColumnFilters);
|
|
1193
|
+
setColumnSizing(settings?.columnSizing ?? emptyColumnSizing);
|
|
1194
|
+
setSorting(settings?.sorting ?? stableInitialSorting);
|
|
1195
|
+
setGlobalFilter(settings?.globalFilter ?? "");
|
|
1196
|
+
setPagination({
|
|
1197
|
+
pageIndex: 0,
|
|
1198
|
+
pageSize: resolvePageSize(
|
|
1199
|
+
settings?.pageSize,
|
|
1200
|
+
initialPageSize,
|
|
1201
|
+
stablePageSizeOptions,
|
|
1202
|
+
),
|
|
1203
|
+
});
|
|
1204
|
+
setPreferencesReady(true);
|
|
1205
|
+
}, [
|
|
1206
|
+
enablePersistence,
|
|
1207
|
+
initialPageSize,
|
|
1208
|
+
stableDefaultColumnOrder,
|
|
1209
|
+
stableInitialColumnFilters,
|
|
1210
|
+
stableInitialColumnOrder,
|
|
1211
|
+
stableInitialColumnVisibility,
|
|
1212
|
+
stableInitialSorting,
|
|
1213
|
+
stableLockedEndColumnIds,
|
|
1214
|
+
stableLockedStartColumnIds,
|
|
1215
|
+
stablePageSizeOptions,
|
|
1216
|
+
storageKey,
|
|
1217
|
+
]);
|
|
1218
|
+
|
|
1219
|
+
React.useEffect(() => {
|
|
1220
|
+
if (!preferencesReady) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
setColumnOrder((currentOrder) =>
|
|
1225
|
+
normalizeColumnOrder(
|
|
1226
|
+
currentOrder,
|
|
1227
|
+
stableDefaultColumnOrder,
|
|
1228
|
+
stableLockedStartColumnIds,
|
|
1229
|
+
stableLockedEndColumnIds,
|
|
1230
|
+
),
|
|
1231
|
+
);
|
|
1232
|
+
}, [
|
|
1233
|
+
preferencesReady,
|
|
1234
|
+
stableDefaultColumnOrder,
|
|
1235
|
+
stableLockedEndColumnIds,
|
|
1236
|
+
stableLockedStartColumnIds,
|
|
1237
|
+
]);
|
|
1238
|
+
|
|
1239
|
+
React.useEffect(() => {
|
|
1240
|
+
if (!enablePersistence || !preferencesReady) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
window.localStorage.setItem(
|
|
1245
|
+
storageKey,
|
|
1246
|
+
JSON.stringify({
|
|
1247
|
+
columnFilters,
|
|
1248
|
+
columnOrder,
|
|
1249
|
+
columnSizing,
|
|
1250
|
+
columnVisibility,
|
|
1251
|
+
globalFilter,
|
|
1252
|
+
pageSize: pagination.pageSize,
|
|
1253
|
+
sorting,
|
|
1254
|
+
} satisfies StoredDataTableSettings),
|
|
1255
|
+
);
|
|
1256
|
+
}, [
|
|
1257
|
+
columnFilters,
|
|
1258
|
+
columnOrder,
|
|
1259
|
+
columnSizing,
|
|
1260
|
+
columnVisibility,
|
|
1261
|
+
enablePersistence,
|
|
1262
|
+
globalFilter,
|
|
1263
|
+
pagination.pageSize,
|
|
1264
|
+
preferencesReady,
|
|
1265
|
+
sorting,
|
|
1266
|
+
storageKey,
|
|
1267
|
+
]);
|
|
1268
|
+
|
|
1269
|
+
React.useEffect(() => {
|
|
1270
|
+
if (!preferencesReady) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
onStateChange?.({
|
|
1275
|
+
columnFilters,
|
|
1276
|
+
globalFilter,
|
|
1277
|
+
pagination,
|
|
1278
|
+
sorting,
|
|
1279
|
+
});
|
|
1280
|
+
}, [
|
|
1281
|
+
columnFilters,
|
|
1282
|
+
globalFilter,
|
|
1283
|
+
onStateChange,
|
|
1284
|
+
pagination,
|
|
1285
|
+
preferencesReady,
|
|
1286
|
+
sorting,
|
|
1287
|
+
]);
|
|
1288
|
+
|
|
1289
|
+
const table = useDataTableInstance<TData>({
|
|
1290
|
+
autoResetPageIndex: !manualPagination,
|
|
1291
|
+
columnResizeMode: "onChange",
|
|
1292
|
+
columns,
|
|
1293
|
+
data,
|
|
1294
|
+
enableColumnResizing: true,
|
|
1295
|
+
enableMultiSort: true,
|
|
1296
|
+
enableRowSelection,
|
|
1297
|
+
filterFns,
|
|
1298
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1299
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
1300
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
1301
|
+
getRowId,
|
|
1302
|
+
getSortedRowModel: getSortedRowModel(),
|
|
1303
|
+
// An explicit undefined would replace TanStack's default and disable search.
|
|
1304
|
+
globalFilterFn: globalFilterFn ?? "includesString",
|
|
1305
|
+
isMultiSortEvent: () => true,
|
|
1306
|
+
manualFiltering,
|
|
1307
|
+
manualPagination,
|
|
1308
|
+
manualSorting,
|
|
1309
|
+
maxMultiSortColCount: 5,
|
|
1310
|
+
meta: {
|
|
1311
|
+
enableExcludeFilters:
|
|
1312
|
+
enableExcludeFilters ?? (!manualFiltering && !onStateChange),
|
|
1313
|
+
},
|
|
1314
|
+
onColumnFiltersChange: handleColumnFiltersChange,
|
|
1315
|
+
onColumnOrderChange: setColumnOrder,
|
|
1316
|
+
onColumnSizingChange: setColumnSizing,
|
|
1317
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
1318
|
+
onGlobalFilterChange: handleGlobalFilterChange,
|
|
1319
|
+
onPaginationChange: setPagination,
|
|
1320
|
+
onRowSelectionChange: setRowSelection,
|
|
1321
|
+
onSortingChange: handleSortingChange,
|
|
1322
|
+
pageCount,
|
|
1323
|
+
rowCount,
|
|
1324
|
+
state: {
|
|
1325
|
+
columnFilters,
|
|
1326
|
+
columnOrder,
|
|
1327
|
+
columnSizing,
|
|
1328
|
+
columnVisibility,
|
|
1329
|
+
globalFilter,
|
|
1330
|
+
pagination,
|
|
1331
|
+
rowSelection,
|
|
1332
|
+
sorting,
|
|
1333
|
+
},
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
1337
|
+
const { active, over } = event;
|
|
1338
|
+
|
|
1339
|
+
if (!over || active.id === over.id) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (
|
|
1344
|
+
isLockedColumn(
|
|
1345
|
+
String(active.id),
|
|
1346
|
+
stableLockedStartColumnIds,
|
|
1347
|
+
stableLockedEndColumnIds,
|
|
1348
|
+
)
|
|
1349
|
+
) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
setColumnOrder((currentOrder) => {
|
|
1354
|
+
const oldIndex = currentOrder.indexOf(String(active.id));
|
|
1355
|
+
const newIndex = currentOrder.indexOf(String(over.id));
|
|
1356
|
+
|
|
1357
|
+
if (oldIndex === -1 || newIndex === -1) {
|
|
1358
|
+
return currentOrder;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return normalizeColumnOrder(
|
|
1362
|
+
arrayMove(currentOrder, oldIndex, newIndex),
|
|
1363
|
+
stableDefaultColumnOrder,
|
|
1364
|
+
stableLockedStartColumnIds,
|
|
1365
|
+
stableLockedEndColumnIds,
|
|
74
1366
|
);
|
|
75
1367
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
const resetColumns = () => {
|
|
1371
|
+
setColumnVisibility(stableInitialColumnVisibility);
|
|
1372
|
+
setColumnOrder(
|
|
1373
|
+
normalizeColumnOrder(
|
|
1374
|
+
stableInitialColumnOrder ?? stableDefaultColumnOrder,
|
|
1375
|
+
stableDefaultColumnOrder,
|
|
1376
|
+
stableLockedStartColumnIds,
|
|
1377
|
+
stableLockedEndColumnIds,
|
|
1378
|
+
),
|
|
1379
|
+
);
|
|
1380
|
+
setColumnSizing(emptyColumnSizing);
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
const resetFilters = React.useCallback(() => {
|
|
1384
|
+
setColumnFilters(stableInitialColumnFilters);
|
|
1385
|
+
setGlobalFilter("");
|
|
1386
|
+
setPagination((current) => ({ ...current, pageIndex: 0 }));
|
|
1387
|
+
}, [stableInitialColumnFilters]);
|
|
1388
|
+
|
|
1389
|
+
React.useEffect(() => {
|
|
1390
|
+
const handleResetFilters = (event: Event) => {
|
|
1391
|
+
const resetStorageKey = getResetFiltersStorageKey(event);
|
|
1392
|
+
|
|
1393
|
+
if (resetStorageKey && resetStorageKey !== storageKey) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
resetFilters();
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
window.addEventListener(DATA_TABLE_RESET_FILTERS_EVENT, handleResetFilters);
|
|
1401
|
+
|
|
1402
|
+
return () => {
|
|
1403
|
+
window.removeEventListener(
|
|
1404
|
+
DATA_TABLE_RESET_FILTERS_EVENT,
|
|
1405
|
+
handleResetFilters,
|
|
1406
|
+
);
|
|
1407
|
+
};
|
|
1408
|
+
}, [resetFilters, storageKey]);
|
|
1409
|
+
|
|
1410
|
+
if (!preferencesReady) {
|
|
1411
|
+
return <DataTableSkeleton />;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const retry = onRetry ? (
|
|
1415
|
+
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
1416
|
+
<RotateCwIcon data-icon="inline-start" />
|
|
1417
|
+
{ui.retry}
|
|
1418
|
+
</Button>
|
|
1419
|
+
) : null;
|
|
1420
|
+
|
|
1421
|
+
if (unavailable) {
|
|
1422
|
+
return (
|
|
1423
|
+
<Empty className="rounded-3xl border border-dashed">
|
|
1424
|
+
<EmptyHeader>
|
|
1425
|
+
<EmptyMedia variant="icon">
|
|
1426
|
+
<LockIcon />
|
|
1427
|
+
</EmptyMedia>
|
|
1428
|
+
<EmptyTitle>{unavailable}</EmptyTitle>
|
|
1429
|
+
</EmptyHeader>
|
|
1430
|
+
</Empty>
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (error && !data.length && !loading) {
|
|
1435
|
+
return (
|
|
1436
|
+
<Empty className="rounded-3xl border border-dashed">
|
|
1437
|
+
<EmptyHeader>
|
|
1438
|
+
<EmptyMedia variant="icon">
|
|
1439
|
+
<CircleAlertIcon />
|
|
1440
|
+
</EmptyMedia>
|
|
1441
|
+
<EmptyTitle>{ui.loadFailed}</EmptyTitle>
|
|
1442
|
+
<EmptyDescription>{error}</EmptyDescription>
|
|
1443
|
+
</EmptyHeader>
|
|
1444
|
+
{retry ? <EmptyContent>{retry}</EmptyContent> : null}
|
|
1445
|
+
</Empty>
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const isFiltered = Boolean(globalFilter) || columnFilters.length > 0;
|
|
1450
|
+
|
|
1451
|
+
const filteredRowCount = rowCount ?? table.getFilteredRowModel().rows.length;
|
|
1452
|
+
const selectedRowCount = table.getFilteredSelectedRowModel().rows.length;
|
|
1453
|
+
const selectedRowsLabel =
|
|
1454
|
+
formatSelectedRowCount ??
|
|
1455
|
+
((count: number) => t.selected(formatRowCount(count), count));
|
|
1456
|
+
const toolbarStartContent = toolbarStart?.(table);
|
|
1457
|
+
const toolbarEndContent = toolbarEnd?.(table);
|
|
1458
|
+
const toolbarBelowContent = toolbarBelow?.(table);
|
|
1459
|
+
const rowSelectionActionsContent = rowSelectionActions?.(table);
|
|
1460
|
+
|
|
1461
|
+
return (
|
|
1462
|
+
<div
|
|
1463
|
+
className={cn(
|
|
1464
|
+
"flex w-full min-w-0 flex-col gap-4",
|
|
1465
|
+
fillHeight && "min-h-0 flex-1",
|
|
1466
|
+
)}
|
|
1467
|
+
>
|
|
1468
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
1469
|
+
{toolbarStartContent}
|
|
1470
|
+
<div className="flex min-w-0 flex-1 basis-80 items-center gap-2">
|
|
1471
|
+
<InputGroup className="flex-1">
|
|
1472
|
+
<InputGroupInput
|
|
1473
|
+
value={globalFilter}
|
|
1474
|
+
onChange={(event) => handleGlobalFilterChange(event.target.value)}
|
|
1475
|
+
placeholder={searchPlaceholder ?? t.search}
|
|
1476
|
+
/>
|
|
1477
|
+
<InputGroupAddon className="pl-2">
|
|
1478
|
+
<SearchIcon />
|
|
1479
|
+
</InputGroupAddon>
|
|
1480
|
+
<InputGroupAddon align="inline-end" className="pr-2">
|
|
1481
|
+
{formatResultCount(filteredRowCount)}
|
|
1482
|
+
</InputGroupAddon>
|
|
1483
|
+
</InputGroup>
|
|
1484
|
+
{toolbarEndContent}
|
|
1485
|
+
<ColumnSettings
|
|
1486
|
+
defaultColumnOrder={defaultColumnOrder}
|
|
1487
|
+
lockedEndColumnIds={lockedEndColumnIds}
|
|
1488
|
+
lockedStartColumnIds={lockedStartColumnIds}
|
|
1489
|
+
onResetColumns={resetColumns}
|
|
1490
|
+
table={table}
|
|
1491
|
+
/>
|
|
1492
|
+
</div>
|
|
97
1493
|
</div>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
1494
|
+
|
|
1495
|
+
{toolbarBelowContent}
|
|
1496
|
+
|
|
1497
|
+
{error ? (
|
|
1498
|
+
<Alert variant="destructive">
|
|
1499
|
+
<CircleAlertIcon />
|
|
1500
|
+
<AlertTitle>{ui.staleData}</AlertTitle>
|
|
1501
|
+
<AlertDescription>{error}</AlertDescription>
|
|
1502
|
+
{retry ? <AlertAction>{retry}</AlertAction> : null}
|
|
1503
|
+
</Alert>
|
|
1504
|
+
) : null}
|
|
1505
|
+
{warning ? (
|
|
1506
|
+
<Alert>
|
|
1507
|
+
<TriangleAlertIcon />
|
|
1508
|
+
<AlertTitle>{warning}</AlertTitle>
|
|
1509
|
+
</Alert>
|
|
1510
|
+
) : null}
|
|
1511
|
+
|
|
1512
|
+
{enableRowSelection && selectedRowCount > 0 && (
|
|
1513
|
+
<div className="bg-muted/40 flex flex-col gap-2 rounded-2xl border p-2 sm:flex-row sm:items-center sm:justify-between">
|
|
1514
|
+
<p className="text-foreground px-1 text-sm font-medium">
|
|
1515
|
+
{selectedRowsLabel(selectedRowCount)}
|
|
1516
|
+
</p>
|
|
1517
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1518
|
+
{rowSelectionActionsContent}
|
|
1519
|
+
<Button
|
|
1520
|
+
type="button"
|
|
1521
|
+
variant="outline"
|
|
1522
|
+
size="sm"
|
|
1523
|
+
onClick={() => table.resetRowSelection()}
|
|
1524
|
+
>
|
|
1525
|
+
<XIcon data-icon="inline-start" />
|
|
1526
|
+
{t.clearSelection}
|
|
1527
|
+
</Button>
|
|
1528
|
+
</div>
|
|
1529
|
+
</div>
|
|
1530
|
+
)}
|
|
1531
|
+
|
|
1532
|
+
<DndContext
|
|
1533
|
+
// Stable id: dnd-kit's global counter differs between server and client.
|
|
1534
|
+
id={dndContextId}
|
|
1535
|
+
collisionDetection={closestCenter}
|
|
1536
|
+
modifiers={[restrictToHorizontalAxis]}
|
|
1537
|
+
onDragEnd={handleDragEnd}
|
|
1538
|
+
sensors={sensors}
|
|
1539
|
+
>
|
|
1540
|
+
<div
|
|
1541
|
+
className={cn(
|
|
1542
|
+
"bg-card overflow-hidden rounded-2xl border",
|
|
1543
|
+
fillHeight && "flex min-h-0 flex-1 flex-col",
|
|
1544
|
+
)}
|
|
1545
|
+
>
|
|
1546
|
+
<Table
|
|
1547
|
+
containerClassName={
|
|
1548
|
+
// Bottom-only fade: a top fade would dissolve the sticky header.
|
|
1549
|
+
fillHeight
|
|
1550
|
+
? "min-h-0 flex-1 overflow-auto scroll-fade-b"
|
|
1551
|
+
: undefined
|
|
1552
|
+
}
|
|
1553
|
+
style={{ minWidth: "100%", width: table.getTotalSize() }}
|
|
1554
|
+
>
|
|
1555
|
+
{/* Solid background: rows scroll underneath the sticky header. */}
|
|
1556
|
+
<TableHeader className="bg-muted sticky top-0 z-10">
|
|
1557
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
1558
|
+
<TableRow key={headerGroup.id}>
|
|
1559
|
+
<SortableContext
|
|
1560
|
+
items={columnOrder}
|
|
1561
|
+
strategy={horizontalListSortingStrategy}
|
|
1562
|
+
>
|
|
1563
|
+
{headerGroup.headers.map((header) => (
|
|
1564
|
+
<DataTableHead
|
|
1565
|
+
key={header.id}
|
|
1566
|
+
header={header}
|
|
1567
|
+
lockedEndColumnIds={lockedEndColumnIds}
|
|
1568
|
+
lockedStartColumnIds={lockedStartColumnIds}
|
|
1569
|
+
/>
|
|
1570
|
+
))}
|
|
1571
|
+
</SortableContext>
|
|
1572
|
+
</TableRow>
|
|
1573
|
+
))}
|
|
1574
|
+
</TableHeader>
|
|
1575
|
+
<TableBody>
|
|
1576
|
+
{loading ? (
|
|
1577
|
+
Array.from({ length: 3 }, (_, index) => (
|
|
1578
|
+
<TableRow key={index}>
|
|
1579
|
+
{table.getVisibleLeafColumns().map((column) => (
|
|
1580
|
+
<TableCell key={column.id}>
|
|
1581
|
+
<Skeleton className="h-5 w-full" />
|
|
1582
|
+
</TableCell>
|
|
1583
|
+
))}
|
|
1584
|
+
</TableRow>
|
|
1585
|
+
))
|
|
1586
|
+
) : table.getRowModel().rows?.length ? (
|
|
1587
|
+
table.getRowModel().rows.map((row) => (
|
|
1588
|
+
<TableRow
|
|
1589
|
+
key={row.id}
|
|
1590
|
+
data-active={row.id === activeRowId || undefined}
|
|
1591
|
+
aria-current={row.id === activeRowId ? "true" : undefined}
|
|
1592
|
+
data-state={row.getIsSelected() && "selected"}
|
|
1593
|
+
className={onRowClick ? "cursor-pointer" : undefined}
|
|
1594
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
1595
|
+
aria-label={
|
|
1596
|
+
onRowClick
|
|
1597
|
+
? `${t.open}: ${getRowLabel?.(row.original) ?? row.id}`
|
|
1598
|
+
: undefined
|
|
1599
|
+
}
|
|
1600
|
+
onClick={
|
|
1601
|
+
onRowClick ? () => onRowClick(row.original) : undefined
|
|
1602
|
+
}
|
|
1603
|
+
onKeyDown={
|
|
1604
|
+
onRowClick
|
|
1605
|
+
? (event) => {
|
|
1606
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1607
|
+
event.preventDefault();
|
|
1608
|
+
onRowClick(row.original);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
: undefined
|
|
1612
|
+
}
|
|
1613
|
+
>
|
|
1614
|
+
{row.getVisibleCells().map((cell) => (
|
|
1615
|
+
<TableCell
|
|
1616
|
+
key={cell.id}
|
|
1617
|
+
className={cn(
|
|
1618
|
+
"whitespace-nowrap",
|
|
1619
|
+
getColumnAlignClass(cell.column),
|
|
1620
|
+
getColumnMeta(cell.column)?.cellClassName,
|
|
1621
|
+
)}
|
|
1622
|
+
style={{ width: cell.column.getSize() }}
|
|
1623
|
+
>
|
|
1624
|
+
{flexRender(
|
|
1625
|
+
cell.column.columnDef.cell,
|
|
1626
|
+
cell.getContext(),
|
|
1627
|
+
)}
|
|
1628
|
+
</TableCell>
|
|
1629
|
+
))}
|
|
1630
|
+
</TableRow>
|
|
1631
|
+
))
|
|
1632
|
+
) : (
|
|
1633
|
+
<TableRow>
|
|
1634
|
+
<TableCell
|
|
1635
|
+
colSpan={table.getVisibleLeafColumns().length}
|
|
1636
|
+
className="text-muted-foreground h-24 text-center"
|
|
1637
|
+
>
|
|
1638
|
+
{isFiltered ? (
|
|
1639
|
+
<Empty>
|
|
1640
|
+
<EmptyHeader>
|
|
1641
|
+
<EmptyTitle>
|
|
1642
|
+
{emptyMessage ?? ui.noResults}
|
|
1643
|
+
</EmptyTitle>
|
|
1644
|
+
</EmptyHeader>
|
|
1645
|
+
<EmptyContent>
|
|
1646
|
+
<Button
|
|
1647
|
+
variant="outline"
|
|
1648
|
+
size="sm"
|
|
1649
|
+
onClick={resetFilters}
|
|
1650
|
+
>
|
|
1651
|
+
{t.clearFilters}
|
|
1652
|
+
</Button>
|
|
1653
|
+
</EmptyContent>
|
|
1654
|
+
</Empty>
|
|
1655
|
+
) : (
|
|
1656
|
+
(emptyState ?? (
|
|
1657
|
+
<Empty>
|
|
1658
|
+
<EmptyHeader>
|
|
1659
|
+
<EmptyMedia variant="icon">
|
|
1660
|
+
<InboxIcon />
|
|
1661
|
+
</EmptyMedia>
|
|
1662
|
+
<EmptyTitle>{emptyMessage ?? ui.noData}</EmptyTitle>
|
|
1663
|
+
</EmptyHeader>
|
|
1664
|
+
</Empty>
|
|
1665
|
+
))
|
|
1666
|
+
)}
|
|
1667
|
+
</TableCell>
|
|
1668
|
+
</TableRow>
|
|
1669
|
+
)}
|
|
1670
|
+
</TableBody>
|
|
1671
|
+
</Table>
|
|
1672
|
+
</div>
|
|
1673
|
+
</DndContext>
|
|
1674
|
+
|
|
1675
|
+
<DataTablePagination
|
|
1676
|
+
formatRowCount={formatRowCount}
|
|
1677
|
+
formatSelectedRowCount={formatSelectedRowCount}
|
|
1678
|
+
pageSizeOptions={stablePageSizeOptions}
|
|
1679
|
+
rowCount={rowCount}
|
|
1680
|
+
storageKey={storageKey}
|
|
1681
|
+
table={table}
|
|
1682
|
+
/>
|
|
1683
|
+
</div>
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function DataTableHead<TData extends RowData>({
|
|
1688
|
+
header,
|
|
1689
|
+
lockedEndColumnIds,
|
|
1690
|
+
lockedStartColumnIds,
|
|
1691
|
+
}: {
|
|
1692
|
+
header: Header<TData, unknown>;
|
|
1693
|
+
lockedEndColumnIds: string[];
|
|
1694
|
+
lockedStartColumnIds: string[];
|
|
1695
|
+
}) {
|
|
1696
|
+
const t = useMessages(dataTableMessages);
|
|
1697
|
+
const column = header.column;
|
|
1698
|
+
const table = header.getContext().table;
|
|
1699
|
+
const align = getColumnAlign(column);
|
|
1700
|
+
const isSorted = column.getIsSorted();
|
|
1701
|
+
const sortIndex = table
|
|
1702
|
+
.getState()
|
|
1703
|
+
.sorting.findIndex((sort) => sort.id === column.id);
|
|
1704
|
+
const isLocked = isLockedColumn(
|
|
1705
|
+
column.id,
|
|
1706
|
+
lockedStartColumnIds,
|
|
1707
|
+
lockedEndColumnIds,
|
|
1708
|
+
);
|
|
1709
|
+
const canOpenColumnMenu = !header.isPlaceholder && !isLocked;
|
|
1710
|
+
const {
|
|
1711
|
+
attributes,
|
|
1712
|
+
isDragging,
|
|
1713
|
+
listeners,
|
|
1714
|
+
setNodeRef,
|
|
1715
|
+
transform,
|
|
1716
|
+
transition,
|
|
1717
|
+
} = useSortable({
|
|
1718
|
+
id: column.id,
|
|
1719
|
+
disabled: isLocked,
|
|
1720
|
+
});
|
|
1721
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
1722
|
+
|
|
1723
|
+
const columnControls =
|
|
1724
|
+
column.getCanSort() || !isLocked ? (
|
|
1725
|
+
<div
|
|
1726
|
+
className={cn(
|
|
1727
|
+
"flex shrink-0 items-center gap-1",
|
|
1728
|
+
align === "right" ? "order-first mr-1" : "ml-1",
|
|
1729
|
+
)}
|
|
1730
|
+
>
|
|
1731
|
+
{column.getCanSort() && (
|
|
1732
|
+
<Button
|
|
1733
|
+
type="button"
|
|
1734
|
+
variant="ghost"
|
|
1735
|
+
size="icon-xs"
|
|
1736
|
+
className={cn(
|
|
1737
|
+
"transition-opacity",
|
|
1738
|
+
isSorted ? "text-foreground bg-accent" : "text-muted-foreground",
|
|
1739
|
+
getHeaderControlVisibilityClass(Boolean(isSorted)),
|
|
1740
|
+
)}
|
|
1741
|
+
onClick={() => toggleColumnSorting(table, column.id)}
|
|
1742
|
+
aria-label={t.toggleSort(getColumnLabel(column))}
|
|
1743
|
+
>
|
|
1744
|
+
<ColumnSortIcon isSorted={isSorted} />
|
|
1745
|
+
</Button>
|
|
1746
|
+
)}
|
|
1747
|
+
{sortIndex >= 0 && table.getState().sorting.length > 1 && (
|
|
1748
|
+
<Badge
|
|
1749
|
+
variant="secondary"
|
|
1750
|
+
className="px-1"
|
|
1751
|
+
title={t.sortCriterionTitle(sortIndex + 1)}
|
|
1752
|
+
>
|
|
1753
|
+
{sortIndex + 1}
|
|
1754
|
+
</Badge>
|
|
1755
|
+
)}
|
|
1756
|
+
{!isLocked && (
|
|
1757
|
+
<Button
|
|
1758
|
+
type="button"
|
|
1759
|
+
variant="ghost"
|
|
1760
|
+
size="icon-xs"
|
|
1761
|
+
className={cn(
|
|
1762
|
+
"text-muted-foreground cursor-grab transition-opacity active:cursor-grabbing",
|
|
1763
|
+
getHeaderControlVisibilityClass(isDragging),
|
|
1764
|
+
)}
|
|
1765
|
+
aria-label={t.reorderColumn(getColumnLabel(column))}
|
|
1766
|
+
{...attributes}
|
|
1767
|
+
{...listeners}
|
|
1768
|
+
>
|
|
1769
|
+
<GripVerticalIcon />
|
|
1770
|
+
</Button>
|
|
1771
|
+
)}
|
|
1772
|
+
</div>
|
|
1773
|
+
) : null;
|
|
1774
|
+
|
|
1775
|
+
return (
|
|
1776
|
+
<TableHead
|
|
1777
|
+
ref={setNodeRef}
|
|
1778
|
+
colSpan={header.colSpan}
|
|
1779
|
+
aria-sort={getColumnAriaSort(isSorted)}
|
|
1780
|
+
className={cn(
|
|
1781
|
+
"relative whitespace-nowrap",
|
|
1782
|
+
getColumnAlignClass(column),
|
|
1783
|
+
getColumnMeta(column)?.headerClassName,
|
|
1784
|
+
isDragging && "bg-muted relative z-20 opacity-80",
|
|
1785
|
+
)}
|
|
1786
|
+
style={{
|
|
1787
|
+
transform: CSS.Translate.toString(transform),
|
|
1788
|
+
transition,
|
|
1789
|
+
width: header.getSize(),
|
|
1790
|
+
}}
|
|
1791
|
+
>
|
|
1792
|
+
{header.isPlaceholder ? null : (
|
|
1793
|
+
<div
|
|
1794
|
+
className={cn(
|
|
1795
|
+
"group/header flex w-full items-center",
|
|
1796
|
+
align === "center" && "justify-center",
|
|
1797
|
+
align === "right" && "justify-end",
|
|
1798
|
+
)}
|
|
1799
|
+
>
|
|
1800
|
+
<div className="flex min-w-0 flex-1 items-center">
|
|
1801
|
+
{canOpenColumnMenu ? (
|
|
1802
|
+
<ColumnHeaderMenu
|
|
1803
|
+
header={header}
|
|
1804
|
+
open={menuOpen}
|
|
1805
|
+
onOpenChange={setMenuOpen}
|
|
1806
|
+
/>
|
|
1807
|
+
) : (
|
|
1808
|
+
<div className="flex h-8 items-center px-0">
|
|
1809
|
+
{flexRender(column.columnDef.header, header.getContext())}
|
|
1810
|
+
</div>
|
|
1811
|
+
)}
|
|
1812
|
+
</div>
|
|
1813
|
+
{columnControls}
|
|
1814
|
+
</div>
|
|
1815
|
+
)}
|
|
1816
|
+
{column.getCanResize() && !isLocked ? (
|
|
1817
|
+
<div
|
|
1818
|
+
role="separator"
|
|
1819
|
+
aria-orientation="vertical"
|
|
1820
|
+
aria-label={t.resizeColumn(getColumnLabel(column))}
|
|
1821
|
+
className={cn(
|
|
1822
|
+
"hover:bg-primary/50 absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize touch-none bg-transparent select-none",
|
|
1823
|
+
column.getIsResizing() && "bg-primary",
|
|
1824
|
+
)}
|
|
1825
|
+
onDoubleClick={() => column.resetSize()}
|
|
1826
|
+
onMouseDown={header.getResizeHandler()}
|
|
1827
|
+
onTouchStart={header.getResizeHandler()}
|
|
1828
|
+
/>
|
|
1829
|
+
) : null}
|
|
1830
|
+
</TableHead>
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function ColumnHeaderMenuTrigger<TData extends RowData>({
|
|
1835
|
+
className,
|
|
1836
|
+
header,
|
|
1837
|
+
}: {
|
|
1838
|
+
className?: string;
|
|
1839
|
+
header: Header<TData, unknown>;
|
|
1840
|
+
}) {
|
|
1841
|
+
const t = useMessages(dataTableMessages);
|
|
1842
|
+
const column = header.column;
|
|
1843
|
+
const align = getColumnAlign(column);
|
|
1844
|
+
const isFiltered = isFilterValueActive(column.getFilterValue());
|
|
1845
|
+
const isExcluding =
|
|
1846
|
+
isFiltered && readMultiFilterValue(column.getFilterValue()).exclude;
|
|
1847
|
+
const ActiveFilterIcon = isExcluding ? FunnelXIcon : FunnelIcon;
|
|
1848
|
+
|
|
1849
|
+
return (
|
|
1850
|
+
<PopoverTrigger
|
|
1851
|
+
render={
|
|
1852
|
+
<Button
|
|
1853
|
+
type="button"
|
|
1854
|
+
variant="ghost"
|
|
1855
|
+
size="sm"
|
|
1856
|
+
className={cn(
|
|
1857
|
+
"relative -mx-2.5 h-8 w-[calc(100%+1.25rem)] min-w-0 justify-start px-2.5",
|
|
1858
|
+
align === "center" && "justify-center",
|
|
1859
|
+
align === "right" && "justify-end",
|
|
1860
|
+
className,
|
|
1861
|
+
)}
|
|
1862
|
+
/>
|
|
1863
|
+
}
|
|
1864
|
+
>
|
|
1865
|
+
<span className="truncate">{getColumnLabel(column)}</span>
|
|
1866
|
+
{isFiltered && (
|
|
1867
|
+
<>
|
|
1868
|
+
<ActiveFilterIcon data-icon="inline-end" aria-hidden />
|
|
1869
|
+
<span className="sr-only">
|
|
1870
|
+
{isExcluding ? t.excludeFilterActive : t.filterActive}
|
|
1871
|
+
</span>
|
|
1872
|
+
</>
|
|
1873
|
+
)}
|
|
1874
|
+
</PopoverTrigger>
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function ColumnSortIcon({ isSorted }: { isSorted: DataTableSortDirection }) {
|
|
1879
|
+
if (isSorted === "asc") {
|
|
1880
|
+
return <ArrowUpNarrowWideIcon data-icon="inline-end" />;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
if (isSorted === "desc") {
|
|
1884
|
+
return <ArrowDownWideNarrowIcon data-icon="inline-end" />;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
return <ArrowUpDownIcon data-icon="inline-end" />;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function ColumnHeaderMenu<TData extends RowData>({
|
|
1891
|
+
header,
|
|
1892
|
+
onOpenChange,
|
|
1893
|
+
open,
|
|
1894
|
+
}: {
|
|
1895
|
+
header: Header<TData, unknown>;
|
|
1896
|
+
onOpenChange: (open: boolean) => void;
|
|
1897
|
+
open: boolean;
|
|
1898
|
+
}) {
|
|
1899
|
+
const t = useMessages(dataTableMessages);
|
|
1900
|
+
const column = header.column;
|
|
1901
|
+
const table = header.getContext().table;
|
|
1902
|
+
const isSorted = column.getIsSorted();
|
|
1903
|
+
const isFiltered = isFilterValueActive(column.getFilterValue());
|
|
1904
|
+
const sorting = table.getState().sorting;
|
|
1905
|
+
const sortIndex = sorting.findIndex((sort) => sort.id === column.id);
|
|
1906
|
+
const sortCount = sorting.length;
|
|
1907
|
+
const hasColumnState = Boolean(isSorted) || isFiltered;
|
|
1908
|
+
|
|
1909
|
+
const clearColumnState = () => {
|
|
1910
|
+
column.setFilterValue(undefined);
|
|
1911
|
+
clearColumnSorting(table, column.id);
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
return (
|
|
1915
|
+
<Popover open={open} onOpenChange={onOpenChange}>
|
|
1916
|
+
<ColumnHeaderMenuTrigger header={header} />
|
|
1917
|
+
<PopoverContent align="start" className="w-72 p-0">
|
|
1918
|
+
<div className="flex flex-col gap-3 p-3">
|
|
1919
|
+
<div className="flex min-w-0 items-start justify-between gap-2">
|
|
1920
|
+
<div className="min-w-0">
|
|
1921
|
+
<div className="truncate text-sm font-medium">
|
|
1922
|
+
{getColumnLabel(column)}
|
|
1923
|
+
</div>
|
|
1924
|
+
</div>
|
|
1925
|
+
{sortIndex >= 0 && sortCount > 1 && (
|
|
1926
|
+
<Badge variant="outline" className="shrink-0">
|
|
1927
|
+
{t.sortCriterion(sortIndex + 1)}
|
|
1928
|
+
</Badge>
|
|
1929
|
+
)}
|
|
1930
|
+
</div>
|
|
1931
|
+
|
|
1932
|
+
{column.getCanSort() && (
|
|
1933
|
+
<ColumnSortControls
|
|
1934
|
+
column={column}
|
|
1935
|
+
isSorted={isSorted}
|
|
1936
|
+
table={table}
|
|
1937
|
+
/>
|
|
1938
|
+
)}
|
|
1939
|
+
|
|
1940
|
+
<ColumnFilterControls
|
|
1941
|
+
allowExclude={Boolean(table.options.meta?.enableExcludeFilters)}
|
|
1942
|
+
column={column}
|
|
1943
|
+
/>
|
|
1944
|
+
|
|
1945
|
+
{(hasColumnState || column.getCanHide()) && (
|
|
1946
|
+
<>
|
|
1947
|
+
<Separator />
|
|
1948
|
+
<div className="flex gap-2">
|
|
1949
|
+
{hasColumnState && (
|
|
113
1950
|
<Button
|
|
114
1951
|
variant="ghost"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
desc: sort?.id === column.id ? !sort.desc : false,
|
|
119
|
-
})
|
|
120
|
-
}
|
|
1952
|
+
size="sm"
|
|
1953
|
+
className="flex-1"
|
|
1954
|
+
onClick={clearColumnState}
|
|
121
1955
|
>
|
|
122
|
-
|
|
123
|
-
{
|
|
124
|
-
sort.desc ? (
|
|
125
|
-
<ArrowDown data-icon="inline-end" />
|
|
126
|
-
) : (
|
|
127
|
-
<ArrowUp data-icon="inline-end" />
|
|
128
|
-
)
|
|
129
|
-
) : null}
|
|
1956
|
+
<XIcon data-icon="inline-start" />
|
|
1957
|
+
{t.clearAll}
|
|
130
1958
|
</Button>
|
|
131
|
-
) : (
|
|
132
|
-
column.header
|
|
133
1959
|
)}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
1960
|
+
{column.getCanHide() && (
|
|
1961
|
+
<Button
|
|
1962
|
+
variant="outline"
|
|
1963
|
+
size="sm"
|
|
1964
|
+
className="flex-1"
|
|
1965
|
+
onClick={() => column.toggleVisibility(false)}
|
|
1966
|
+
>
|
|
1967
|
+
<EyeOffIcon data-icon="inline-start" />
|
|
1968
|
+
{t.hide}
|
|
1969
|
+
</Button>
|
|
1970
|
+
)}
|
|
1971
|
+
</div>
|
|
1972
|
+
</>
|
|
1973
|
+
)}
|
|
1974
|
+
</div>
|
|
1975
|
+
</PopoverContent>
|
|
1976
|
+
</Popover>
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// Pressed option inside the column menu; the selected state uses the primary
|
|
1981
|
+
// fill plus a check so it stays distinguishable in both themes.
|
|
1982
|
+
function ColumnMenuOption({
|
|
1983
|
+
buttonRef,
|
|
1984
|
+
children,
|
|
1985
|
+
onClick,
|
|
1986
|
+
selected,
|
|
1987
|
+
}: {
|
|
1988
|
+
buttonRef?: React.Ref<HTMLButtonElement>;
|
|
1989
|
+
children: React.ReactNode;
|
|
1990
|
+
onClick: () => void;
|
|
1991
|
+
selected: boolean;
|
|
1992
|
+
}) {
|
|
1993
|
+
return (
|
|
1994
|
+
<Button
|
|
1995
|
+
ref={buttonRef}
|
|
1996
|
+
variant={selected ? "default" : "outline"}
|
|
1997
|
+
size="sm"
|
|
1998
|
+
className="justify-start"
|
|
1999
|
+
aria-pressed={selected}
|
|
2000
|
+
onClick={onClick}
|
|
2001
|
+
>
|
|
2002
|
+
{children}
|
|
2003
|
+
{selected && <CheckIcon data-icon="inline-end" className="ml-auto" />}
|
|
2004
|
+
</Button>
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function ColumnSortControls<TData extends RowData>({
|
|
2009
|
+
column,
|
|
2010
|
+
isSorted,
|
|
2011
|
+
table,
|
|
2012
|
+
}: {
|
|
2013
|
+
column: Column<TData, unknown>;
|
|
2014
|
+
isSorted: false | "asc" | "desc";
|
|
2015
|
+
table: TanstackTable<TData>;
|
|
2016
|
+
}) {
|
|
2017
|
+
const t = useMessages(dataTableMessages);
|
|
2018
|
+
const ascendingRef = React.useRef<HTMLButtonElement>(null);
|
|
2019
|
+
|
|
2020
|
+
return (
|
|
2021
|
+
<div className="flex flex-col gap-2">
|
|
2022
|
+
<div className="flex min-h-6 items-center justify-between gap-2">
|
|
2023
|
+
<Label className="text-muted-foreground text-xs">{t.sort}</Label>
|
|
2024
|
+
{isSorted && (
|
|
2025
|
+
<Button
|
|
2026
|
+
variant="ghost"
|
|
2027
|
+
size="xs"
|
|
2028
|
+
onClick={() => {
|
|
2029
|
+
clearColumnSorting(table, column.id);
|
|
2030
|
+
ascendingRef.current?.focus();
|
|
2031
|
+
}}
|
|
2032
|
+
>
|
|
2033
|
+
<XIcon data-icon="inline-start" />
|
|
2034
|
+
{t.removeSort}
|
|
2035
|
+
</Button>
|
|
2036
|
+
)}
|
|
2037
|
+
</div>
|
|
2038
|
+
<div className="grid grid-cols-2 gap-2" role="group" aria-label={t.sort}>
|
|
2039
|
+
<ColumnMenuOption
|
|
2040
|
+
buttonRef={ascendingRef}
|
|
2041
|
+
selected={isSorted === "asc"}
|
|
2042
|
+
onClick={() => setColumnSorting(table, column.id, false)}
|
|
2043
|
+
>
|
|
2044
|
+
<ArrowUpNarrowWideIcon data-icon="inline-start" />
|
|
2045
|
+
{t.ascending}
|
|
2046
|
+
</ColumnMenuOption>
|
|
2047
|
+
<ColumnMenuOption
|
|
2048
|
+
selected={isSorted === "desc"}
|
|
2049
|
+
onClick={() => setColumnSorting(table, column.id, true)}
|
|
2050
|
+
>
|
|
2051
|
+
<ArrowDownWideNarrowIcon data-icon="inline-start" />
|
|
2052
|
+
{t.descending}
|
|
2053
|
+
</ColumnMenuOption>
|
|
2054
|
+
</div>
|
|
2055
|
+
</div>
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
function ColumnFilterControls<TData extends RowData>({
|
|
2060
|
+
allowExclude,
|
|
2061
|
+
column,
|
|
2062
|
+
}: {
|
|
2063
|
+
allowExclude: boolean;
|
|
2064
|
+
column: Column<TData, unknown>;
|
|
2065
|
+
}) {
|
|
2066
|
+
const t = useMessages(dataTableMessages);
|
|
2067
|
+
const filter = getColumnMeta(column)?.filter;
|
|
2068
|
+
|
|
2069
|
+
if (!filter) {
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (filter.type === "custom") {
|
|
2074
|
+
return filter.render(column);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
if (filter.type === "multi") {
|
|
2078
|
+
return (
|
|
2079
|
+
<ColumnMultiFilter
|
|
2080
|
+
allowExclude={allowExclude}
|
|
2081
|
+
column={column}
|
|
2082
|
+
options={filter.options}
|
|
2083
|
+
/>
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (filter.type === "number-range") {
|
|
2088
|
+
const range = readRangeFilterValue(column.getFilterValue());
|
|
2089
|
+
|
|
2090
|
+
return (
|
|
2091
|
+
<div className="flex flex-col gap-2">
|
|
2092
|
+
<Label className="text-muted-foreground text-xs">{t.filter}</Label>
|
|
2093
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2094
|
+
<Input
|
|
2095
|
+
type="number"
|
|
2096
|
+
min={filter.min}
|
|
2097
|
+
max={filter.max}
|
|
2098
|
+
value={range.min ?? ""}
|
|
2099
|
+
onChange={(event) =>
|
|
2100
|
+
setObjectFilterValue(column, "min", event.target.value)
|
|
2101
|
+
}
|
|
2102
|
+
placeholder={filter.minPlaceholder ?? t.min}
|
|
2103
|
+
/>
|
|
2104
|
+
<Input
|
|
2105
|
+
type="number"
|
|
2106
|
+
min={filter.min}
|
|
2107
|
+
max={filter.max}
|
|
2108
|
+
value={range.max ?? ""}
|
|
2109
|
+
onChange={(event) =>
|
|
2110
|
+
setObjectFilterValue(column, "max", event.target.value)
|
|
2111
|
+
}
|
|
2112
|
+
placeholder={filter.maxPlaceholder ?? t.max}
|
|
2113
|
+
/>
|
|
2114
|
+
</div>
|
|
2115
|
+
</div>
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
if (filter.type === "date-range") {
|
|
2120
|
+
const range = readDateRangeFilterValue(column.getFilterValue());
|
|
2121
|
+
|
|
2122
|
+
return (
|
|
2123
|
+
<div className="flex flex-col gap-2">
|
|
2124
|
+
<Label className="text-muted-foreground text-xs">{t.filter}</Label>
|
|
2125
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2126
|
+
<Input
|
|
2127
|
+
type="date"
|
|
2128
|
+
value={range.from ?? ""}
|
|
2129
|
+
onChange={(event) =>
|
|
2130
|
+
setObjectFilterValue(column, "from", event.target.value)
|
|
2131
|
+
}
|
|
2132
|
+
/>
|
|
2133
|
+
<Input
|
|
2134
|
+
type="date"
|
|
2135
|
+
value={range.to ?? ""}
|
|
2136
|
+
onChange={(event) =>
|
|
2137
|
+
setObjectFilterValue(column, "to", event.target.value)
|
|
2138
|
+
}
|
|
2139
|
+
/>
|
|
2140
|
+
</div>
|
|
2141
|
+
</div>
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
return <ColumnTextFilter column={column} placeholder={filter.placeholder} />;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function ColumnTextFilter<TData extends RowData>({
|
|
2149
|
+
column,
|
|
2150
|
+
placeholder,
|
|
2151
|
+
}: {
|
|
2152
|
+
column: Column<TData, unknown>;
|
|
2153
|
+
placeholder?: string;
|
|
2154
|
+
}) {
|
|
2155
|
+
const t = useMessages(dataTableMessages);
|
|
2156
|
+
const label = getColumnLabel(column).toLocaleLowerCase();
|
|
2157
|
+
|
|
2158
|
+
return (
|
|
2159
|
+
<div className="flex flex-col gap-2">
|
|
2160
|
+
<Label className="text-muted-foreground text-xs">{t.filter}</Label>
|
|
2161
|
+
<Input
|
|
2162
|
+
value={
|
|
2163
|
+
readStringProperty({ value: column.getFilterValue() }, "value") ?? ""
|
|
2164
|
+
}
|
|
2165
|
+
onChange={(event) =>
|
|
2166
|
+
column.setFilterValue(event.target.value || undefined)
|
|
2167
|
+
}
|
|
2168
|
+
placeholder={placeholder ?? t.filterBy(label)}
|
|
2169
|
+
/>
|
|
2170
|
+
</div>
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
function ColumnMultiFilter<TData extends RowData>({
|
|
2175
|
+
allowExclude,
|
|
2176
|
+
column,
|
|
2177
|
+
options,
|
|
2178
|
+
}: {
|
|
2179
|
+
allowExclude: boolean;
|
|
2180
|
+
column: Column<TData, unknown>;
|
|
2181
|
+
options: DataTableFilterOption[];
|
|
2182
|
+
}) {
|
|
2183
|
+
const t = useMessages(dataTableMessages);
|
|
2184
|
+
const stored = readMultiFilterValue(column.getFilterValue());
|
|
2185
|
+
const [pendingExclude, setPendingExclude] = React.useState(stored.exclude);
|
|
2186
|
+
const selected = stored.values;
|
|
2187
|
+
const exclude =
|
|
2188
|
+
allowExclude && (selected.length ? stored.exclude : pendingExclude);
|
|
2189
|
+
|
|
2190
|
+
const toggleValue = (value: string, checked: boolean) => {
|
|
2191
|
+
const next = checked
|
|
2192
|
+
? [...selected, value]
|
|
2193
|
+
: selected.filter((item) => item !== value);
|
|
2194
|
+
|
|
2195
|
+
column.setFilterValue(toMultiFilterValue(exclude, next));
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2198
|
+
const setMode = (nextExclude: boolean) => {
|
|
2199
|
+
setPendingExclude(nextExclude);
|
|
2200
|
+
column.setFilterValue(toMultiFilterValue(nextExclude, selected));
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
return (
|
|
2204
|
+
<div className="flex flex-col gap-2">
|
|
2205
|
+
<Label className="text-muted-foreground text-xs">{t.filter}</Label>
|
|
2206
|
+
{allowExclude && (
|
|
2207
|
+
<div
|
|
2208
|
+
className="grid grid-cols-2 gap-2"
|
|
2209
|
+
role="group"
|
|
2210
|
+
aria-label={t.filterMode}
|
|
2211
|
+
>
|
|
2212
|
+
<ColumnMenuOption selected={!exclude} onClick={() => setMode(false)}>
|
|
2213
|
+
<FunnelIcon data-icon="inline-start" />
|
|
2214
|
+
{t.show}
|
|
2215
|
+
</ColumnMenuOption>
|
|
2216
|
+
<ColumnMenuOption selected={exclude} onClick={() => setMode(true)}>
|
|
2217
|
+
<FunnelXIcon data-icon="inline-start" />
|
|
2218
|
+
{t.exclude}
|
|
2219
|
+
</ColumnMenuOption>
|
|
2220
|
+
</div>
|
|
2221
|
+
)}
|
|
2222
|
+
<div
|
|
2223
|
+
className="flex max-h-56 scroll-fade flex-col gap-1 overflow-auto rounded-2xl border p-1"
|
|
2224
|
+
role="group"
|
|
2225
|
+
aria-label={exclude ? t.valuesToExclude : t.valuesToShow}
|
|
2226
|
+
>
|
|
2227
|
+
{options.map((option) => (
|
|
2228
|
+
<label
|
|
2229
|
+
key={option.value}
|
|
2230
|
+
className="hover:bg-accent flex items-center gap-2 rounded-xl px-2 py-1.5 text-sm font-normal"
|
|
2231
|
+
>
|
|
2232
|
+
<Checkbox
|
|
2233
|
+
checked={selected.includes(option.value)}
|
|
2234
|
+
onCheckedChange={(checked) =>
|
|
2235
|
+
toggleValue(option.value, Boolean(checked))
|
|
2236
|
+
}
|
|
2237
|
+
/>
|
|
2238
|
+
<span
|
|
2239
|
+
className={cn(
|
|
2240
|
+
"min-w-0 flex-1 truncate",
|
|
2241
|
+
exclude &&
|
|
2242
|
+
selected.includes(option.value) &&
|
|
2243
|
+
"text-muted-foreground line-through",
|
|
2244
|
+
)}
|
|
2245
|
+
>
|
|
2246
|
+
{option.label}
|
|
2247
|
+
</span>
|
|
2248
|
+
</label>
|
|
2249
|
+
))}
|
|
2250
|
+
</div>
|
|
2251
|
+
</div>
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function ColumnSettings<TData extends RowData>({
|
|
2256
|
+
defaultColumnOrder,
|
|
2257
|
+
lockedEndColumnIds,
|
|
2258
|
+
lockedStartColumnIds,
|
|
2259
|
+
onResetColumns,
|
|
2260
|
+
table,
|
|
2261
|
+
}: {
|
|
2262
|
+
defaultColumnOrder: ColumnOrderState;
|
|
2263
|
+
lockedEndColumnIds: string[];
|
|
2264
|
+
lockedStartColumnIds: string[];
|
|
2265
|
+
onResetColumns: () => void;
|
|
2266
|
+
table: TanstackTable<TData>;
|
|
2267
|
+
}) {
|
|
2268
|
+
const t = useMessages(dataTableMessages);
|
|
2269
|
+
const hiddenColumnCount = table
|
|
2270
|
+
.getAllLeafColumns()
|
|
2271
|
+
.filter((column) => column.getCanHide())
|
|
2272
|
+
.filter((column) => !column.getIsVisible()).length;
|
|
2273
|
+
const visibleColumnsCustomized = hiddenColumnCount > 0;
|
|
2274
|
+
const columnOrderCustomized = !areColumnOrdersEqual(
|
|
2275
|
+
normalizeColumnOrder(
|
|
2276
|
+
table.getState().columnOrder,
|
|
2277
|
+
defaultColumnOrder,
|
|
2278
|
+
lockedStartColumnIds,
|
|
2279
|
+
lockedEndColumnIds,
|
|
2280
|
+
),
|
|
2281
|
+
normalizeColumnOrder(
|
|
2282
|
+
defaultColumnOrder,
|
|
2283
|
+
defaultColumnOrder,
|
|
2284
|
+
lockedStartColumnIds,
|
|
2285
|
+
lockedEndColumnIds,
|
|
2286
|
+
),
|
|
2287
|
+
);
|
|
2288
|
+
const columnsCustomized = visibleColumnsCustomized || columnOrderCustomized;
|
|
2289
|
+
|
|
2290
|
+
return (
|
|
2291
|
+
<TooltipProvider>
|
|
2292
|
+
<Tooltip>
|
|
2293
|
+
<DropdownMenu>
|
|
2294
|
+
<TooltipTrigger
|
|
2295
|
+
render={
|
|
2296
|
+
<DropdownMenuTrigger
|
|
2297
|
+
render={
|
|
2298
|
+
<Button
|
|
2299
|
+
variant="outline"
|
|
2300
|
+
size="icon-sm"
|
|
2301
|
+
className="relative"
|
|
2302
|
+
aria-label={t.customizeColumns}
|
|
2303
|
+
/>
|
|
2304
|
+
}
|
|
2305
|
+
/>
|
|
2306
|
+
}
|
|
2307
|
+
>
|
|
2308
|
+
<Columns3Icon />
|
|
2309
|
+
{hiddenColumnCount > 0 && (
|
|
2310
|
+
<span className="bg-primary absolute -top-0.5 -right-0.5 size-2 rounded-full" />
|
|
2311
|
+
)}
|
|
2312
|
+
</TooltipTrigger>
|
|
2313
|
+
<TooltipContent side="bottom" align="end">
|
|
2314
|
+
{t.columnsTooltip}
|
|
2315
|
+
</TooltipContent>
|
|
2316
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
2317
|
+
<DropdownMenuGroup>
|
|
2318
|
+
<DropdownMenuLabel>{t.columns}</DropdownMenuLabel>
|
|
2319
|
+
{table
|
|
2320
|
+
.getAllLeafColumns()
|
|
2321
|
+
.filter((column) => column.getCanHide())
|
|
2322
|
+
.map((column) => (
|
|
2323
|
+
<DropdownMenuCheckboxItem
|
|
2324
|
+
key={column.id}
|
|
2325
|
+
checked={column.getIsVisible()}
|
|
2326
|
+
onCheckedChange={(value) =>
|
|
2327
|
+
column.toggleVisibility(!!value)
|
|
2328
|
+
}
|
|
2329
|
+
closeOnClick={false}
|
|
2330
|
+
>
|
|
2331
|
+
{getColumnLabel(column)}
|
|
2332
|
+
</DropdownMenuCheckboxItem>
|
|
164
2333
|
))}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
2334
|
+
</DropdownMenuGroup>
|
|
2335
|
+
{columnsCustomized && (
|
|
2336
|
+
<>
|
|
2337
|
+
<DropdownMenuSeparator />
|
|
2338
|
+
<DropdownMenuItem onClick={onResetColumns}>
|
|
2339
|
+
<XIcon />
|
|
2340
|
+
{t.resetColumns}
|
|
2341
|
+
</DropdownMenuItem>
|
|
2342
|
+
</>
|
|
2343
|
+
)}
|
|
2344
|
+
</DropdownMenuContent>
|
|
2345
|
+
</DropdownMenu>
|
|
2346
|
+
</Tooltip>
|
|
2347
|
+
</TooltipProvider>
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function DataTablePagination<TData extends RowData>({
|
|
2352
|
+
formatRowCount,
|
|
2353
|
+
formatSelectedRowCount,
|
|
2354
|
+
pageSizeOptions,
|
|
2355
|
+
rowCount,
|
|
2356
|
+
storageKey,
|
|
2357
|
+
table,
|
|
2358
|
+
}: {
|
|
2359
|
+
formatRowCount: (count: number) => string;
|
|
2360
|
+
formatSelectedRowCount?: (count: number) => string;
|
|
2361
|
+
pageSizeOptions: number[];
|
|
2362
|
+
rowCount?: number;
|
|
2363
|
+
storageKey: string;
|
|
2364
|
+
table: TanstackTable<TData>;
|
|
2365
|
+
}) {
|
|
2366
|
+
const t = useMessages(dataTableMessages);
|
|
2367
|
+
const selectedRows = table.getFilteredSelectedRowModel().rows.length;
|
|
2368
|
+
const filteredRows = rowCount ?? table.getFilteredRowModel().rows.length;
|
|
2369
|
+
const totalRows = rowCount ?? table.getCoreRowModel().rows.length;
|
|
2370
|
+
const selectedRowsLabel =
|
|
2371
|
+
formatSelectedRowCount ??
|
|
2372
|
+
((count: number) => t.selected(formatRowCount(count), count));
|
|
2373
|
+
const fallbackId = React.useId();
|
|
2374
|
+
const inputId = storageKey
|
|
2375
|
+
? `${getStorageSafeId(storageKey)}-rows-per-page`
|
|
2376
|
+
: fallbackId;
|
|
2377
|
+
let paginationSummary = t.ofSummary(
|
|
2378
|
+
formatRowCount(filteredRows),
|
|
2379
|
+
formatRowCount(totalRows),
|
|
2380
|
+
);
|
|
2381
|
+
|
|
2382
|
+
if (selectedRows > 0) {
|
|
2383
|
+
paginationSummary = t.ofSummary(
|
|
2384
|
+
selectedRowsLabel(selectedRows),
|
|
2385
|
+
formatRowCount(filteredRows),
|
|
2386
|
+
);
|
|
2387
|
+
} else if (filteredRows === totalRows) {
|
|
2388
|
+
paginationSummary = `${formatRowCount(totalRows)}.`;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
return (
|
|
2392
|
+
<div className="flex flex-col gap-3 px-1 sm:flex-row sm:items-center sm:justify-between">
|
|
2393
|
+
<div className="text-muted-foreground text-sm">{paginationSummary}</div>
|
|
2394
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
2395
|
+
<div className="flex items-center gap-2">
|
|
2396
|
+
<Label htmlFor={inputId} className="text-sm">
|
|
2397
|
+
{t.rowsPerPage}
|
|
2398
|
+
</Label>
|
|
2399
|
+
<Select
|
|
2400
|
+
value={`${table.getState().pagination.pageSize}`}
|
|
2401
|
+
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
2402
|
+
>
|
|
2403
|
+
<SelectTrigger id={inputId} size="sm" className="w-20">
|
|
2404
|
+
<SelectValue />
|
|
2405
|
+
</SelectTrigger>
|
|
2406
|
+
<SelectContent side="top">
|
|
2407
|
+
{pageSizeOptions.map((pageSize) => (
|
|
2408
|
+
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
2409
|
+
{pageSize}
|
|
2410
|
+
</SelectItem>
|
|
2411
|
+
))}
|
|
2412
|
+
</SelectContent>
|
|
2413
|
+
</Select>
|
|
2414
|
+
</div>
|
|
2415
|
+
<div className="text-sm font-medium">
|
|
2416
|
+
{t.page(
|
|
2417
|
+
table.getState().pagination.pageIndex + 1,
|
|
2418
|
+
Math.max(table.getPageCount(), 1),
|
|
192
2419
|
)}
|
|
193
|
-
</
|
|
194
|
-
|
|
195
|
-
<div className="wb-table-footer">
|
|
196
|
-
<span>
|
|
197
|
-
{selected.length} registros · página {currentPage + 1} de{" "}
|
|
198
|
-
{lastPage + 1}
|
|
199
|
-
</span>
|
|
200
|
-
<div className="flex gap-2">
|
|
2420
|
+
</div>
|
|
2421
|
+
<div className="flex items-center gap-2">
|
|
201
2422
|
<Button
|
|
202
2423
|
variant="outline"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
2424
|
+
className="hidden sm:flex"
|
|
2425
|
+
size="icon"
|
|
2426
|
+
onClick={() => table.setPageIndex(0)}
|
|
2427
|
+
disabled={!table.getCanPreviousPage()}
|
|
207
2428
|
>
|
|
208
|
-
<
|
|
209
|
-
|
|
2429
|
+
<span className="sr-only">{t.firstPage}</span>
|
|
2430
|
+
<ChevronsLeftIcon />
|
|
210
2431
|
</Button>
|
|
211
2432
|
<Button
|
|
212
2433
|
variant="outline"
|
|
213
|
-
size="
|
|
214
|
-
|
|
215
|
-
disabled={
|
|
216
|
-
onClick={() => setPage(currentPage + 1)}
|
|
2434
|
+
size="icon"
|
|
2435
|
+
onClick={() => table.previousPage()}
|
|
2436
|
+
disabled={!table.getCanPreviousPage()}
|
|
217
2437
|
>
|
|
218
|
-
|
|
219
|
-
<
|
|
2438
|
+
<span className="sr-only">{t.previousPage}</span>
|
|
2439
|
+
<ChevronLeftIcon />
|
|
220
2440
|
</Button>
|
|
2441
|
+
<Button
|
|
2442
|
+
variant="outline"
|
|
2443
|
+
size="icon"
|
|
2444
|
+
onClick={() => table.nextPage()}
|
|
2445
|
+
disabled={!table.getCanNextPage()}
|
|
2446
|
+
>
|
|
2447
|
+
<span className="sr-only">{t.nextPage}</span>
|
|
2448
|
+
<ChevronRightIcon />
|
|
2449
|
+
</Button>
|
|
2450
|
+
<Button
|
|
2451
|
+
variant="outline"
|
|
2452
|
+
className="hidden sm:flex"
|
|
2453
|
+
size="icon"
|
|
2454
|
+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
2455
|
+
disabled={!table.getCanNextPage()}
|
|
2456
|
+
>
|
|
2457
|
+
<span className="sr-only">{t.lastPage}</span>
|
|
2458
|
+
<ChevronsRightIcon />
|
|
2459
|
+
</Button>
|
|
2460
|
+
</div>
|
|
2461
|
+
</div>
|
|
2462
|
+
</div>
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
export function DataTableSkeleton() {
|
|
2467
|
+
return (
|
|
2468
|
+
<div className="flex w-full flex-col gap-4" aria-hidden="true">
|
|
2469
|
+
<div className="flex items-center gap-2">
|
|
2470
|
+
<Skeleton className="h-9 flex-1 rounded-3xl" />
|
|
2471
|
+
<Skeleton className="size-8 rounded-full" />
|
|
2472
|
+
</div>
|
|
2473
|
+
<div className="bg-card overflow-hidden rounded-2xl border">
|
|
2474
|
+
<Skeleton className="h-10 rounded-none" />
|
|
2475
|
+
<div className="flex flex-col">
|
|
2476
|
+
{Array.from({ length: 8 }).map((_, index) => (
|
|
2477
|
+
<Skeleton
|
|
2478
|
+
key={index}
|
|
2479
|
+
className="bg-muted/40 h-14 rounded-none border-t"
|
|
2480
|
+
/>
|
|
2481
|
+
))}
|
|
221
2482
|
</div>
|
|
222
2483
|
</div>
|
|
223
|
-
</
|
|
2484
|
+
</div>
|
|
224
2485
|
);
|
|
225
2486
|
}
|