@ciridae/ui 0.3.7 → 0.5.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/dist/components/ui/data-table-column-settings.d.ts +23 -0
- package/dist/components/ui/data-table-column-settings.js +170 -0
- package/dist/components/ui/data-table-context.d.ts +8 -0
- package/dist/components/ui/data-table-context.js +54 -0
- package/dist/components/ui/data-table-facet-filter.d.ts +23 -0
- package/dist/components/ui/data-table-facet-filter.js +53 -0
- package/dist/components/ui/data-table-grid.js +13 -4
- package/dist/components/ui/data-table-pagination.d.ts +9 -2
- package/dist/components/ui/data-table-pagination.js +80 -10
- package/dist/components/ui/data-table-state.d.ts +19 -0
- package/dist/components/ui/data-table-state.js +8 -0
- package/dist/components/ui/data-table-tanstack.d.ts +41 -3
- package/dist/components/ui/data-table-tanstack.js +58 -2
- package/dist/components/ui/data-table.js +5 -1
- package/dist/components/ui/sidebar/layout.js +6 -1
- package/dist/components/ui/table.d.ts +12 -1
- package/dist/components/ui/table.js +8 -3
- package/dist/entries/data-table-column-settings.d.ts +1 -0
- package/dist/entries/data-table-column-settings.js +1 -0
- package/dist/entries/data-table-context.d.ts +1 -0
- package/dist/entries/data-table-context.js +1 -0
- package/dist/entries/data-table-facet-filter.d.ts +1 -0
- package/dist/entries/data-table-facet-filter.js +1 -0
- package/dist/entries/data-table-state.d.ts +1 -0
- package/dist/entries/data-table-state.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { RowData } from "@tanstack/react-table";
|
|
3
|
+
declare module "@tanstack/react-table" {
|
|
4
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
5
|
+
/** What to call this column where its header cannot be read as text. */
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Choose which columns appear and in what order.
|
|
11
|
+
*
|
|
12
|
+
* A draft the reader commits rather than a live edit. Rearranging a table is
|
|
13
|
+
* several changes that only mean something together, and applying each one as
|
|
14
|
+
* it is made redraws the table under the hand still working on it. Cancel has
|
|
15
|
+
* to be able to mean "none of that", so nothing here touches the table until
|
|
16
|
+
* Apply.
|
|
17
|
+
*/
|
|
18
|
+
declare function DataTableColumnSettings({ description, title, triggerLabel, }: {
|
|
19
|
+
description?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
triggerLabel?: string;
|
|
22
|
+
}): React.JSX.Element | null;
|
|
23
|
+
export { DataTableColumnSettings };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core";
|
|
4
|
+
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
5
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
6
|
+
import { GripVerticalIcon, SlidersHorizontalIcon } from "lucide-react";
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
import { useDataTable } from "./data-table-context";
|
|
9
|
+
import { Button } from "./button";
|
|
10
|
+
import { Checkbox } from "./checkbox";
|
|
11
|
+
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "./sheet";
|
|
12
|
+
/**
|
|
13
|
+
* Choose which columns appear and in what order.
|
|
14
|
+
*
|
|
15
|
+
* A draft the reader commits rather than a live edit. Rearranging a table is
|
|
16
|
+
* several changes that only mean something together, and applying each one as
|
|
17
|
+
* it is made redraws the table under the hand still working on it. Cancel has
|
|
18
|
+
* to be able to mean "none of that", so nothing here touches the table until
|
|
19
|
+
* Apply.
|
|
20
|
+
*/
|
|
21
|
+
function DataTableColumnSettings({ description = "Choose which columns appear and drag them into the order you prefer.", title = "Column settings", triggerLabel = "Settings", }) {
|
|
22
|
+
const table = useDataTable();
|
|
23
|
+
const [open, setOpen] = React.useState(false);
|
|
24
|
+
// `null` means "whatever the table is showing"; anything else is an edit in
|
|
25
|
+
// progress. Cleared on every open and close, so a cancelled edit cannot come
|
|
26
|
+
// back the next time the sheet is opened.
|
|
27
|
+
const [draft, setDraft] = React.useState(null);
|
|
28
|
+
const columns = table.getAllLeafColumns();
|
|
29
|
+
// Reordering stands on its own, so a table where nothing may be hidden still
|
|
30
|
+
// has something to offer here.
|
|
31
|
+
if (columns.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const current = draft ?? readColumns(table);
|
|
34
|
+
return (_jsxs(Sheet, { onOpenChange: (next) => {
|
|
35
|
+
setDraft(null);
|
|
36
|
+
setOpen(next);
|
|
37
|
+
}, open: open, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", type: "button", variant: "ghost", children: [_jsx(SlidersHorizontalIcon, {}), _jsx("span", { children: triggerLabel })] }) }), _jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: title }), _jsx(SheetDescription, { children: description })] }), _jsx(SheetBody, { className: "pb-(--panel-padding)", children: _jsx(ColumnDraftList, { columns: columns, draft: current, onDraftChange: setDraft }) }), _jsxs(SheetFooter, { children: [_jsx(Button, { className: "mr-auto", onClick: () => {
|
|
38
|
+
setDraft(readDefaultColumns(table));
|
|
39
|
+
}, type: "button", variant: "ghost", children: "Reset" }), _jsx(Button, { onClick: () => {
|
|
40
|
+
setOpen(false);
|
|
41
|
+
}, type: "button", variant: "outline", children: "Cancel" }), _jsx(Button, { onClick: () => {
|
|
42
|
+
applyColumns(table, current);
|
|
43
|
+
setOpen(false);
|
|
44
|
+
}, type: "button", children: "Apply" })] })] })] }));
|
|
45
|
+
}
|
|
46
|
+
function ColumnDraftList({ columns, draft, onDraftChange, }) {
|
|
47
|
+
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
48
|
+
const order = draft.order;
|
|
49
|
+
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
50
|
+
const reorder = (event) => {
|
|
51
|
+
const { active, over } = event;
|
|
52
|
+
if (!over || active.id === over.id)
|
|
53
|
+
return;
|
|
54
|
+
const from = order.indexOf(String(active.id));
|
|
55
|
+
const to = order.indexOf(String(over.id));
|
|
56
|
+
if (from === -1 || to === -1)
|
|
57
|
+
return;
|
|
58
|
+
onDraftChange({ ...draft, order: arrayMove(order, from, to) });
|
|
59
|
+
};
|
|
60
|
+
return (_jsx(DndContext, { collisionDetection: closestCenter, onDragEnd: reorder, sensors: sensors, children: _jsx(SortableContext, { items: order, strategy: verticalListSortingStrategy, children: _jsx("ul", { className: "grid gap-0.5", children: order.map((id) => {
|
|
61
|
+
const column = byId.get(id);
|
|
62
|
+
if (column === undefined)
|
|
63
|
+
return null;
|
|
64
|
+
return (_jsx(ColumnSettingsRow, { column: column, hidden: draft.hidden.includes(id), onToggle: (visible) => {
|
|
65
|
+
onDraftChange({
|
|
66
|
+
hidden: visible
|
|
67
|
+
? draft.hidden.filter((entry) => entry !== id)
|
|
68
|
+
: [...draft.hidden, id],
|
|
69
|
+
order,
|
|
70
|
+
});
|
|
71
|
+
} }, id));
|
|
72
|
+
}) }) }) }));
|
|
73
|
+
}
|
|
74
|
+
function ColumnSettingsRow({ column, hidden, onToggle, }) {
|
|
75
|
+
const label = getColumnLabel(column);
|
|
76
|
+
const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
|
|
77
|
+
id: column.id,
|
|
78
|
+
});
|
|
79
|
+
return (_jsxs("li", { className: cn("flex items-center gap-3 rounded-sm px-1 py-2 text-sm", isDragging && "relative z-10 bg-accent"), ref: setNodeRef, style: { transform: CSS.Translate.toString(transform), transition }, children: [_jsx("button", { "aria-label": `Reorder ${label}`, className: "cursor-grab text-muted-foreground outline-none focus-visible:ring-[var(--focus-ring-width)] focus-visible:ring-ring/50", type: "button", ...attributes, ...listeners, children: _jsx(GripVerticalIcon, { className: "size-4" }) }), _jsx(Checkbox, { checked: !hidden, disabled: !column.getCanHide(), id: `column-setting-${column.id}`, onCheckedChange: (checked) => {
|
|
80
|
+
onToggle(checked === true);
|
|
81
|
+
} }), _jsx("label", { className: "min-w-0 flex-1 truncate", htmlFor: `column-setting-${column.id}`, children: label })] }));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* What the table is showing now, in the shape the draft edits.
|
|
85
|
+
*
|
|
86
|
+
* Both fields are spelled out rather than left empty to mean "unchanged": the
|
|
87
|
+
* list has to draw an order either way, and an empty one would have to be
|
|
88
|
+
* filled in from somewhere -- from the current order, which would make Reset
|
|
89
|
+
* write back the very arrangement it was asked to undo.
|
|
90
|
+
*/
|
|
91
|
+
function readColumns(table) {
|
|
92
|
+
const columns = table.getAllLeafColumns();
|
|
93
|
+
return {
|
|
94
|
+
hidden: columns.filter((column) => !column.getIsVisible()).map((column) => column.id),
|
|
95
|
+
order: columns.map((column) => column.id),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* What Reset goes back to: the caller's own defaults, which `TanStackDataTable`
|
|
100
|
+
* keeps in `initialState` for exactly this.
|
|
101
|
+
*
|
|
102
|
+
* Not "every column, as declared" -- a table given a `defaultColumnVisibility`
|
|
103
|
+
* starts with something hidden on purpose, and a Reset that revealed it would
|
|
104
|
+
* be showing a view the caller never offered.
|
|
105
|
+
*
|
|
106
|
+
* A `columnOrder` is a partial list in TanStack: it names the columns it cares
|
|
107
|
+
* about and everything else keeps its declared position after them. The draft
|
|
108
|
+
* cannot be partial, because the list draws exactly what the draft names -- so
|
|
109
|
+
* a default naming two of five columns would make the other three vanish from
|
|
110
|
+
* the sheet, unreorderable and unhideable, and Apply would then write that
|
|
111
|
+
* two-column order back. Reconciling against the live leaf ids keeps the
|
|
112
|
+
* caller's prefix and restores the rest behind it.
|
|
113
|
+
*/
|
|
114
|
+
function readDefaultColumns(table) {
|
|
115
|
+
const { columnOrder, columnVisibility } = table.initialState;
|
|
116
|
+
// Declared order, not drawn order: `getAllLeafColumns` has already had the
|
|
117
|
+
// current order applied to it, so resetting through it would return the
|
|
118
|
+
// arrangement Reset was asked to undo.
|
|
119
|
+
const declared = table
|
|
120
|
+
.getAllFlatColumns()
|
|
121
|
+
.filter((column) => column.columns.length === 0)
|
|
122
|
+
.map((column) => column.id);
|
|
123
|
+
const named = (columnOrder ?? []).filter((id) => declared.includes(id));
|
|
124
|
+
return {
|
|
125
|
+
hidden: Object.entries(columnVisibility ?? {})
|
|
126
|
+
.filter(([, visible]) => !visible)
|
|
127
|
+
.map(([id]) => id),
|
|
128
|
+
order: [...named, ...declared.filter((id) => !named.includes(id))],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Commit a draft to the table.
|
|
133
|
+
*
|
|
134
|
+
* Back to the defaults goes through the table's own reset rather than through
|
|
135
|
+
* an equivalent-looking explicit order. Writing the full list would leave a
|
|
136
|
+
* caller mirroring this into a URL with an order parameter saying "arranged"
|
|
137
|
+
* for a view that is not, and would pin today's columns so a later release
|
|
138
|
+
* appended its new one at the end rather than where it was declared.
|
|
139
|
+
*/
|
|
140
|
+
function applyColumns(table, draft) {
|
|
141
|
+
if (isDefaultColumns(draft, readDefaultColumns(table))) {
|
|
142
|
+
table.resetColumnOrder();
|
|
143
|
+
table.resetColumnVisibility();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
table.setColumnOrder(draft.order);
|
|
147
|
+
table.setColumnVisibility(Object.fromEntries(draft.hidden.map((id) => [id, false])));
|
|
148
|
+
}
|
|
149
|
+
/** Whether a draft is the default arrangement, in whatever order it names it. */
|
|
150
|
+
function isDefaultColumns(draft, defaults) {
|
|
151
|
+
return (draft.order.length === defaults.order.length &&
|
|
152
|
+
draft.order.every((id, index) => defaults.order[index] === id) &&
|
|
153
|
+
draft.hidden.length === defaults.hidden.length &&
|
|
154
|
+
defaults.hidden.every((id) => draft.hidden.includes(id)));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* What to call a column in a list of columns.
|
|
158
|
+
*
|
|
159
|
+
* A header rendered as a component -- a select-all checkbox, an icon -- has no
|
|
160
|
+
* text to borrow, and the column id behind it is a name for code rather than
|
|
161
|
+
* for a reader. `meta.label` is where a caller says what to call it instead.
|
|
162
|
+
*/
|
|
163
|
+
function getColumnLabel(column) {
|
|
164
|
+
const label = column.columnDef.meta?.label;
|
|
165
|
+
if (typeof label === "string")
|
|
166
|
+
return label;
|
|
167
|
+
const header = column.columnDef.header;
|
|
168
|
+
return typeof header === "string" ? header : column.id;
|
|
169
|
+
}
|
|
170
|
+
export { DataTableColumnSettings };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Table as TanStackTable } from "@tanstack/react-table";
|
|
3
|
+
declare function DataTableProvider<TData>({ children, table, }: {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
table: TanStackTable<TData>;
|
|
6
|
+
}): React.JSX.Element;
|
|
7
|
+
declare function useDataTable<TData>(): TanStackTable<TData>;
|
|
8
|
+
export { DataTableProvider, useDataTable };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* The table instance `TanStackDataTable` builds, published so the controls
|
|
5
|
+
* placed in its toolbar can read it.
|
|
6
|
+
*
|
|
7
|
+
* The alternative is a function-valued `toolbar` prop, which reads to lint as a
|
|
8
|
+
* component defined during render and would put that error in front of every
|
|
9
|
+
* consumer of a facet filter.
|
|
10
|
+
*
|
|
11
|
+
* Held as `unknown` because a context cannot be generic. Nothing is lost: the
|
|
12
|
+
* only value ever put in is the table `TanStackDataTable` just created, and the
|
|
13
|
+
* only readers are inside that provider, so the row type `useDataTable`
|
|
14
|
+
* restores is the one the caller's own columns were built from.
|
|
15
|
+
*/
|
|
16
|
+
const DataTableContext = React.createContext(null);
|
|
17
|
+
function DataTableProvider({ children, table, }) {
|
|
18
|
+
// A fresh object every render, deliberately, and the one thing here that must
|
|
19
|
+
// not be memoised.
|
|
20
|
+
//
|
|
21
|
+
// A control like `DataTableFacetFilter` takes only static props -- a column
|
|
22
|
+
// id, a title, a list of options -- and reads everything that changes from
|
|
23
|
+
// this context. In an app compiled with React Compiler that makes its element
|
|
24
|
+
// cacheable: no reactive props, so the element is built once and React skips
|
|
25
|
+
// re-rendering it. Publishing the table object itself would close the other
|
|
26
|
+
// door too, because its identity never changes however much its state does,
|
|
27
|
+
// so context propagation would never fire either and the control would render
|
|
28
|
+
// once and then show the table as it was at mount forever.
|
|
29
|
+
//
|
|
30
|
+
// Wrapping restores propagation: the provider re-renders whenever the table's
|
|
31
|
+
// state changes, the value is new each time, and React re-renders every
|
|
32
|
+
// consumer regardless of what the compiler did to their elements.
|
|
33
|
+
//
|
|
34
|
+
// `state` is carried so a reader can see what the value is meant to track,
|
|
35
|
+
// and because TanStack rebuilds its state object each render where the table
|
|
36
|
+
// object is the same one forever.
|
|
37
|
+
//
|
|
38
|
+
// It is NOT a guard against this package one day being built with React
|
|
39
|
+
// Compiler. The compiler takes its dependencies from the sources of an
|
|
40
|
+
// expression, and the only source of `table.getState()` is `table` -- so it
|
|
41
|
+
// would cache the call on `[table]`, cache this literal on the result, and
|
|
42
|
+
// freeze the whole thing exactly as before. If that day comes, this
|
|
43
|
+
// component needs a real bailout, not another field.
|
|
44
|
+
return (_jsx(DataTableContext.Provider, { value: { state: table.getState(), table }, children: children }));
|
|
45
|
+
}
|
|
46
|
+
function useDataTable() {
|
|
47
|
+
const context = React.useContext(DataTableContext);
|
|
48
|
+
if (context === null) {
|
|
49
|
+
throw new Error("This control must be rendered inside a <TanStackDataTable />.");
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line no-restricted-syntax -- restores the row type erased by the context above; the provider only ever stores a TanStackTable.
|
|
52
|
+
return context.table;
|
|
53
|
+
}
|
|
54
|
+
export { DataTableProvider, useDataTable };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type DataTableFacetOption = {
|
|
3
|
+
icon?: React.ComponentType<{
|
|
4
|
+
className?: string;
|
|
5
|
+
}>;
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Narrow a column to a set of its own values.
|
|
11
|
+
*
|
|
12
|
+
* Counts come from the table's faceted row model, which `TanStackDataTable`
|
|
13
|
+
* already builds -- so an option can say how many rows it would leave without
|
|
14
|
+
* the caller counting anything itself. Pair with the `arrIncludesSome` filter
|
|
15
|
+
* function, which is what a multi-select selection means.
|
|
16
|
+
*/
|
|
17
|
+
declare function DataTableFacetFilter({ columnId, options, title, }: {
|
|
18
|
+
columnId: string;
|
|
19
|
+
options: DataTableFacetOption[];
|
|
20
|
+
title: string;
|
|
21
|
+
}): React.JSX.Element;
|
|
22
|
+
export { DataTableFacetFilter };
|
|
23
|
+
export type { DataTableFacetOption };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CheckIcon, PlusCircleIcon } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { useDataTable } from "./data-table-context";
|
|
5
|
+
import { Badge } from "./badge";
|
|
6
|
+
import { Button } from "./button";
|
|
7
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "./command";
|
|
8
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
9
|
+
import { Separator } from "./separator";
|
|
10
|
+
/**
|
|
11
|
+
* Narrow a column to a set of its own values.
|
|
12
|
+
*
|
|
13
|
+
* Counts come from the table's faceted row model, which `TanStackDataTable`
|
|
14
|
+
* already builds -- so an option can say how many rows it would leave without
|
|
15
|
+
* the caller counting anything itself. Pair with the `arrIncludesSome` filter
|
|
16
|
+
* function, which is what a multi-select selection means.
|
|
17
|
+
*/
|
|
18
|
+
function DataTableFacetFilter({ columnId, options, title, }) {
|
|
19
|
+
const column = useDataTable().getColumn(columnId);
|
|
20
|
+
const counts = column?.getFacetedUniqueValues();
|
|
21
|
+
const selected = new Set(readSelection(column?.getFilterValue()));
|
|
22
|
+
const select = (value) => {
|
|
23
|
+
const next = new Set(selected);
|
|
24
|
+
if (next.has(value))
|
|
25
|
+
next.delete(value);
|
|
26
|
+
else
|
|
27
|
+
next.add(value);
|
|
28
|
+
// An empty selection is no filter at all, not a filter matching nothing.
|
|
29
|
+
column?.setFilterValue(next.size ? [...next] : undefined);
|
|
30
|
+
};
|
|
31
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", type: "button", variant: "outline", children: [_jsx(PlusCircleIcon, {}), title, selected.size > 0 ? (_jsxs(_Fragment, { children: [_jsx(Separator, { className: "mx-0.5 data-[orientation=vertical]:h-4", orientation: "vertical" }), _jsx(FacetSelectionSummary, { labels: options.filter((option) => selected.has(option.value)) })] })) : null] }) }), _jsx(PopoverContent, { align: "start", className: "w-52 p-0", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: title }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No matches." }), _jsx(CommandGroup, { children: options.map((option) => (_jsx(FacetOptionItem, { count: counts?.get(option.value), onSelect: () => {
|
|
32
|
+
select(option.value);
|
|
33
|
+
}, option: option, selected: selected.has(option.value) }, option.value))) }), selected.size > 0 ? (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsx(CommandItem, { className: "justify-center", onSelect: () => {
|
|
34
|
+
column?.setFilterValue(undefined);
|
|
35
|
+
}, children: "Clear" }) })] })) : null] })] }) })] }));
|
|
36
|
+
}
|
|
37
|
+
/** Names the choices while they still fit, then counts them. */
|
|
38
|
+
function FacetSelectionSummary({ labels }) {
|
|
39
|
+
if (labels.length > 2)
|
|
40
|
+
return _jsxs(Badge, { variant: "secondary", children: [labels.length, " selected"] });
|
|
41
|
+
return (_jsx(_Fragment, { children: labels.map((option) => (_jsx(Badge, { variant: "secondary", children: option.label }, option.value))) }));
|
|
42
|
+
}
|
|
43
|
+
function FacetOptionItem({ count, onSelect, option, selected, }) {
|
|
44
|
+
const Icon = option.icon;
|
|
45
|
+
return (_jsxs(CommandItem, { onSelect: onSelect, children: [_jsx("span", { "aria-hidden": true, className: cn("grid size-4 place-content-center rounded-[4px] border border-input", selected ? "border-primary bg-primary text-primary-foreground" : "opacity-50"), children: selected ? _jsx(CheckIcon, { className: "size-3.5" }) : null }), Icon ? _jsx(Icon, { className: "size-4 text-muted-foreground" }) : null, _jsx("span", { className: "truncate", children: option.label }), count === undefined ? null : (_jsx("span", { className: "ml-auto font-mono text-xs tabular-nums text-muted-foreground", children: count }))] }));
|
|
46
|
+
}
|
|
47
|
+
/** A column filter value is `unknown`; only an array of strings is a selection. */
|
|
48
|
+
function readSelection(value) {
|
|
49
|
+
if (!Array.isArray(value))
|
|
50
|
+
return [];
|
|
51
|
+
return value.filter((entry) => typeof entry === "string");
|
|
52
|
+
}
|
|
53
|
+
export { DataTableFacetFilter };
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowDownIcon,
|
|
2
|
+
import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon } from "lucide-react";
|
|
3
3
|
import { flexRender, } from "@tanstack/react-table";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
5
|
import { Button } from "./button";
|
|
6
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
|
|
7
7
|
function DataTableGrid({ empty, getRowClassName, onRowClick, rows, table, }) {
|
|
8
|
-
|
|
8
|
+
// One scroller, owned by `data-table-content` outside this: nesting an
|
|
9
|
+
// `overflow-x-auto` inside it would give the sticky header a scrollport that
|
|
10
|
+
// only ever scrolls sideways, so the header would ride away with the rows.
|
|
11
|
+
return (_jsxs(Table, { scrollable: false, children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", children: table.getHeaderGroups().map((group) => (_jsx(TableRow, { children: group.headers.map((header) => (_jsx(TableHead, { style: { width: header.getSize() }, children: header.isPlaceholder ? null : _jsx(DataTableHeaderCell, { header: header }) }, header.id))) }, group.id))) }), _jsx(TableBody, { children: rows.length ? (rows.map((row) => (_jsx(DataTableDataRow, { getRowClassName: getRowClassName, onRowClick: onRowClick, row: row }, row.id)))) : (_jsx(TableRow, { children: _jsx(TableCell, { className: "h-32 text-center text-sm text-muted-foreground", colSpan: table.getVisibleLeafColumns().length, children: empty }) })) })] }));
|
|
9
12
|
}
|
|
10
13
|
function DataTableDataRow({ getRowClassName, onRowClick, row, }) {
|
|
11
14
|
return (_jsx(TableRow, { className: cn(onRowClick && "cursor-pointer", getRowClassName?.(row)), "data-state": row.getIsSelected() ? "selected" : undefined, onClick: onRowClick ? () => onRowClick(row) : undefined, children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }));
|
|
12
15
|
}
|
|
13
16
|
function DataTableHeaderCell({ header }) {
|
|
14
17
|
const sorted = header.column.getIsSorted();
|
|
15
|
-
|
|
18
|
+
// Paired chevrons while unsorted, a single arrow once sorted: the pair reads
|
|
19
|
+
// as "this can go either way", the arrow as "it went this way".
|
|
20
|
+
const Icon = sorted === "asc" ? ArrowUpIcon : sorted === "desc" ? ArrowDownIcon : ChevronsUpDownIcon;
|
|
16
21
|
if (!header.column.getCanSort())
|
|
17
22
|
return flexRender(header.column.columnDef.header, header.getContext());
|
|
18
|
-
return (_jsxs(Button, { "aria-pressed": Boolean(sorted),
|
|
23
|
+
return (_jsxs(Button, { "aria-pressed": Boolean(sorted),
|
|
24
|
+
// `TableHead`'s size and weight, restated rather than inherited --
|
|
25
|
+
// Tailwind has no font-size or font-weight `inherit` utility -- so a
|
|
26
|
+
// sortable column and a plain one read the same.
|
|
27
|
+
className: "-ml-2 h-auto px-2 py-0 text-sm font-normal text-muted-foreground hover:bg-transparent hover:text-foreground", onClick: header.column.getToggleSortingHandler(), size: "sm", type: "button", variant: "ghost", children: [_jsx("span", { children: flexRender(header.column.columnDef.header, header.getContext()) }), _jsx(Icon, {})] }));
|
|
19
28
|
}
|
|
20
29
|
export { DataTableGrid };
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { Table as TanStackTable } from "@tanstack/react-table";
|
|
2
|
-
import type { ResolvedDataTableLabels } from "./data-table-tanstack";
|
|
3
|
-
declare function DataTablePagination<TData>({ labels, pageSizeOptions, selectedRows, table, totalRows, }: {
|
|
2
|
+
import type { DataTablePaginationVariant, ResolvedDataTableLabels } from "./data-table-tanstack";
|
|
3
|
+
declare function DataTablePagination<TData>({ footerControls, labels, pageSizeOptions, selectedRows, table, totalRows, variant, }: {
|
|
4
|
+
/**
|
|
5
|
+
* Controls that belong beside the row count rather than above the table --
|
|
6
|
+
* column settings, an export. Whatever governs the whole result set reads
|
|
7
|
+
* better next to the number saying how big it is.
|
|
8
|
+
*/
|
|
9
|
+
footerControls?: React.ReactNode;
|
|
4
10
|
labels: ResolvedDataTableLabels;
|
|
5
11
|
pageSizeOptions: number[];
|
|
6
12
|
selectedRows: number;
|
|
7
13
|
table: TanStackTable<TData>;
|
|
8
14
|
totalRows: number;
|
|
15
|
+
variant?: DataTablePaginationVariant;
|
|
9
16
|
}): import("@types/react").JSX.Element;
|
|
10
17
|
export { DataTablePagination };
|
|
@@ -1,19 +1,89 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, } from "lucide-react";
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { NativeSelect, NativeSelectOption } from "./native-select";
|
|
5
|
-
|
|
5
|
+
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem } from "./pagination";
|
|
6
|
+
function DataTablePagination({ footerControls, labels, pageSizeOptions, selectedRows, table, totalRows, variant = "compact", }) {
|
|
6
7
|
const pagination = table.getState().pagination;
|
|
7
|
-
|
|
8
|
+
// `-1` is TanStack's "unknown", which a server-driven table reports until it
|
|
9
|
+
// is told a total. Clamping is right for the labels -- a reader is on page 1
|
|
10
|
+
// of at least 1 -- but a control that jumps to the end needs the difference.
|
|
11
|
+
const totalPages = table.getPageCount();
|
|
12
|
+
const pageCount = Math.max(totalPages, 1);
|
|
8
13
|
const page = Math.min(pagination.pageIndex + 1, pageCount);
|
|
9
14
|
const rowsShown = table.getRowModel().rows.length;
|
|
10
|
-
return (_jsxs("div", { "data-slot": "data-table-pagination", className: "flex flex-col gap-2 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between", children: [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
return (_jsxs("div", { "data-slot": "data-table-pagination", className: "flex flex-col gap-2 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "min-w-0 truncate", children: selectedRows > 0
|
|
16
|
+
? (labels.rowsSelected?.(selectedRows, totalRows) ??
|
|
17
|
+
`${selectedRows} of ${totalRows} row(s) selected.`)
|
|
18
|
+
: (labels.rowsShown?.(rowsShown, totalRows) ??
|
|
19
|
+
`${rowsShown} of ${totalRows} row(s) shown.`) }), footerControls] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx("span", { className: "shrink-0", children: labels.rowsPerPage }), _jsx(NativeSelect, { "aria-label": labels.rowsPerPageName, className: "min-w-20 shrink-0", onChange: (event) => table.setPageSize(Number(event.target.value)), size: "sm", value: pagination.pageSize, children: pageSizeOptions.map((pageSize) => (_jsx(NativeSelectOption, { value: pageSize, children: pageSize }, pageSize))) }), variant === "numbered" ? (_jsx(DataTablePager, { labels: labels, page: page, pageCount: pageCount, pageCountKnown: totalPages >= 0, table: table })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-20 text-center tabular-nums", children: labels.page?.(page, pageCount) ?? `Page ${page} of ${pageCount}` }), _jsx(DataTablePaginationButtons, { labels: labels, pageCountKnown: totalPages >= 0, table: table })] }))] })] }));
|
|
15
20
|
}
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Pages as a numbered row rather than a step counter.
|
|
23
|
+
*
|
|
24
|
+
* Worth the extra width when a reader has a page in mind -- page 7 of a member
|
|
25
|
+
* directory is one click, not six. `pageCount` is the clamped count the label
|
|
26
|
+
* uses, so an out-of-range index highlights nothing rather than inventing a
|
|
27
|
+
* page that is not there.
|
|
28
|
+
*/
|
|
29
|
+
function DataTablePager({ labels, page, pageCount, pageCountKnown, table, }) {
|
|
30
|
+
const back = !table.getCanPreviousPage();
|
|
31
|
+
const forward = !table.getCanNextPage();
|
|
32
|
+
return (_jsx(Pagination, { className: "mx-0 w-auto justify-end", children: _jsxs(PaginationContent, { children: [_jsx(PagerStepButton, { disabled: back, icon: ChevronsLeftIcon, label: labels.firstPage, onClick: () => {
|
|
33
|
+
table.firstPage();
|
|
34
|
+
} }), _jsx(PagerStepButton, { disabled: back, icon: ChevronLeftIcon, label: labels.previousPage, onClick: () => {
|
|
35
|
+
table.previousPage();
|
|
36
|
+
} }), _jsx(PagerNumbers, { page: page, pageCount: pageCount, table: table }), _jsx(PagerStepButton, { disabled: forward, icon: ChevronRightIcon, label: labels.nextPage, onClick: () => {
|
|
37
|
+
table.nextPage();
|
|
38
|
+
} }), pageCountKnown ? (_jsx(PagerStepButton, { disabled: forward, icon: ChevronsRightIcon, label: labels.lastPage, onClick: () => {
|
|
39
|
+
table.lastPage();
|
|
40
|
+
} })) : null] }) }));
|
|
41
|
+
}
|
|
42
|
+
/** The numbers themselves, with an ellipsis wherever the row elides a run. */
|
|
43
|
+
function PagerNumbers({ page, pageCount, table, }) {
|
|
44
|
+
return (_jsx(_Fragment, { children: getPagerSteps(page, pageCount).map((step, index) => (_jsx(PaginationItem, { children: step === "gap" ? (_jsx(PaginationEllipsis, { className: "size-8" })) : (_jsx(Button, { "aria-current": step === page ? "page" : undefined, "aria-label": `Page ${String(step)}`, className: "tabular-nums", onClick: () => {
|
|
45
|
+
table.setPageIndex(step - 1);
|
|
46
|
+
}, size: "icon-sm", type: "button", variant: step === page ? "outline" : "ghost", children: step })) }, step === "gap" ? `gap-${String(index)}` : step))) }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One of the four steps either side of the numbers, which differ only in these.
|
|
50
|
+
*
|
|
51
|
+
* Ghost rather than outlined: a row of six bordered boxes reads as six equal
|
|
52
|
+
* things, and the only one worth pointing at is the page you are on.
|
|
53
|
+
*/
|
|
54
|
+
function PagerStepButton({ disabled, icon: Icon, label, onClick, }) {
|
|
55
|
+
return (_jsx(PaginationItem, { children: _jsx(Button, { "aria-label": label, disabled: disabled, onClick: onClick, size: "icon-sm", type: "button", variant: "ghost", children: _jsx(Icon, {}) }) }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The page numbers to draw: always the first and last, always the current and
|
|
59
|
+
* its neighbours, and a gap wherever that skips more than one page.
|
|
60
|
+
*
|
|
61
|
+
* The row is always seven slots wide or fewer, however many pages there are.
|
|
62
|
+
*/
|
|
63
|
+
function getPagerSteps(page, pageCount) {
|
|
64
|
+
if (pageCount <= 7)
|
|
65
|
+
return countUp(1, pageCount);
|
|
66
|
+
let from = Math.min(Math.max(page - 1, 2), pageCount - 4);
|
|
67
|
+
let to = Math.max(Math.min(page + 1, pageCount - 1), 5);
|
|
68
|
+
// A gap hiding a single page is wider than the page it hides, so widen the
|
|
69
|
+
// window over it instead. Only one side can be one short of the end at a
|
|
70
|
+
// time above seven pages, so this cannot push the row past seven slots.
|
|
71
|
+
if (from === 3)
|
|
72
|
+
from = 2;
|
|
73
|
+
if (to === pageCount - 2)
|
|
74
|
+
to = pageCount - 1;
|
|
75
|
+
return [
|
|
76
|
+
1,
|
|
77
|
+
...(from > 2 ? ["gap"] : []),
|
|
78
|
+
...countUp(from, to),
|
|
79
|
+
...(to < pageCount - 1 ? ["gap"] : []),
|
|
80
|
+
pageCount,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
function countUp(from, to) {
|
|
84
|
+
return Array.from({ length: to - from + 1 }, (_, index) => from + index);
|
|
85
|
+
}
|
|
86
|
+
function DataTablePaginationButtons({ labels, pageCountKnown, table, }) {
|
|
87
|
+
return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { "aria-label": labels.firstPage, disabled: !table.getCanPreviousPage(), onClick: () => table.firstPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronsLeftIcon, {}) }), _jsx(Button, { "aria-label": labels.previousPage, disabled: !table.getCanPreviousPage(), onClick: () => table.previousPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronLeftIcon, {}) }), _jsx(Button, { "aria-label": labels.nextPage, disabled: !table.getCanNextPage(), onClick: () => table.nextPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronRightIcon, {}) }), pageCountKnown ? (_jsx(Button, { "aria-label": labels.lastPage, disabled: !table.getCanNextPage(), onClick: () => table.lastPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronsRightIcon, {}) })) : null] }));
|
|
18
88
|
}
|
|
19
89
|
export { DataTablePagination };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary a controlled `TanStackDataTable` is driven in.
|
|
3
|
+
*
|
|
4
|
+
* `TanStackDataTableProps` already exposes these types publicly -- a consumer
|
|
5
|
+
* cannot write `onSortingChange` or a `ColumnDef` without naming them. Without
|
|
6
|
+
* this module the only way to name them is to depend on `@tanstack/react-table`
|
|
7
|
+
* directly, which makes every consumer responsible for keeping a version in
|
|
8
|
+
* step with this package's. Re-exporting them keeps the table library an
|
|
9
|
+
* implementation detail on this side of the boundary.
|
|
10
|
+
*/
|
|
11
|
+
export type { Cell, Column, ColumnDef, ColumnFiltersState, ColumnOrderState, Header, OnChangeFn, PaginationState, Row, RowSelectionState, SortingState, Table, TableOptions, Updater, VisibilityState, } from "@tanstack/react-table";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve an `Updater` to the value it stands for.
|
|
14
|
+
*
|
|
15
|
+
* Every controlled handler receives either the next state or a function
|
|
16
|
+
* producing it from the current one, so a consumer that stores state elsewhere
|
|
17
|
+
* -- a URL, a store -- has to collapse that fork before it can write anywhere.
|
|
18
|
+
*/
|
|
19
|
+
export { functionalUpdate } from "@tanstack/react-table";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an `Updater` to the value it stands for.
|
|
3
|
+
*
|
|
4
|
+
* Every controlled handler receives either the next state or a function
|
|
5
|
+
* producing it from the current one, so a consumer that stores state elsewhere
|
|
6
|
+
* -- a URL, a store -- has to collapse that fork before it can write anywhere.
|
|
7
|
+
*/
|
|
8
|
+
export { functionalUpdate } from "@tanstack/react-table";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { type ColumnDef, type ColumnFiltersState, type OnChangeFn, type PaginationState, type Row, type RowSelectionState, type SortingState, type TableOptions, type VisibilityState } from "@tanstack/react-table";
|
|
2
|
+
import { type ColumnDef, type ColumnFiltersState, type ColumnOrderState, type OnChangeFn, type PaginationState, type Row, type RowSelectionState, type SortingState, type TableOptions, type VisibilityState } from "@tanstack/react-table";
|
|
3
3
|
type DataTableState = {
|
|
4
4
|
columnFilters?: ColumnFiltersState;
|
|
5
|
+
columnOrder?: ColumnOrderState;
|
|
5
6
|
columnVisibility?: VisibilityState;
|
|
6
7
|
globalFilter?: string;
|
|
7
8
|
pagination?: PaginationState;
|
|
@@ -12,28 +13,57 @@ type DataTableLabels = {
|
|
|
12
13
|
columns?: string;
|
|
13
14
|
empty?: React.ReactNode;
|
|
14
15
|
filterPlaceholder?: string;
|
|
16
|
+
firstPage?: string;
|
|
17
|
+
lastPage?: string;
|
|
15
18
|
nextPage?: string;
|
|
16
19
|
page?: (page: number, pageCount: number) => string;
|
|
17
20
|
previousPage?: string;
|
|
21
|
+
/** Sits before the page-size control, which reads "Show 25" rather than "25 rows". */
|
|
22
|
+
rowsPerPage?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The page-size control's accessible name. Separate from `rowsPerPage`
|
|
25
|
+
* because that one is terse by design: "Show" beside a number says enough
|
|
26
|
+
* with the footer in view and nothing at all read out on its own.
|
|
27
|
+
*/
|
|
28
|
+
rowsPerPageName?: string;
|
|
18
29
|
rowsSelected?: (selected: number, total: number) => string;
|
|
19
30
|
rowsShown?: (shown: number, total: number) => string;
|
|
20
31
|
};
|
|
21
32
|
type DataTableFilterMode = "global" | "column" | false;
|
|
33
|
+
type DataTablePaginationVariant = "compact" | "numbered";
|
|
22
34
|
type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps<"div">, "children"> & {
|
|
23
35
|
actions?: React.ReactNode;
|
|
36
|
+
clampPageIndex?: boolean;
|
|
24
37
|
columns: ColumnDef<TData, TValue>[];
|
|
25
38
|
data: TData[];
|
|
39
|
+
defaultColumnOrder?: ColumnOrderState;
|
|
26
40
|
defaultColumnVisibility?: VisibilityState;
|
|
27
41
|
defaultPageSize?: number;
|
|
28
42
|
empty?: React.ReactNode;
|
|
29
43
|
enableColumnFilters?: boolean;
|
|
30
44
|
enableColumnVisibility?: boolean;
|
|
31
45
|
enableGlobalFilter?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether columns may be hidden at all, separately from whether the built-in
|
|
48
|
+
* columns menu is offered. They were the same switch while the menu was the
|
|
49
|
+
* only way to hide a column; a caller supplying its own control needs to turn
|
|
50
|
+
* the menu off without also making every column permanent.
|
|
51
|
+
*/
|
|
52
|
+
enableHiding?: boolean;
|
|
32
53
|
enablePagination?: boolean;
|
|
33
54
|
enableRowSelection?: TableOptions<TData>["enableRowSelection"];
|
|
34
55
|
enableSorting?: boolean;
|
|
35
56
|
filterMode?: DataTableFilterMode;
|
|
36
57
|
filterPlaceholder?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Controls for the footer, beside the row count. Whatever governs the whole
|
|
60
|
+
* result set -- column settings, an export -- reads better next to the number
|
|
61
|
+
* saying how big it is than stacked above the table with the filters.
|
|
62
|
+
*
|
|
63
|
+
* The footer is the pagination row, so `enablePagination={false}` removes
|
|
64
|
+
* these along with it; put them in `actions` for a table that does not page.
|
|
65
|
+
*/
|
|
66
|
+
footerControls?: React.ReactNode;
|
|
37
67
|
getRowClassName?: (row: Row<TData>) => string | undefined;
|
|
38
68
|
getRowId?: TableOptions<TData>["getRowId"];
|
|
39
69
|
labels?: DataTableLabels;
|
|
@@ -42,20 +72,28 @@ type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps
|
|
|
42
72
|
manualPagination?: boolean;
|
|
43
73
|
manualSorting?: boolean;
|
|
44
74
|
onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>;
|
|
75
|
+
onColumnOrderChange?: OnChangeFn<ColumnOrderState>;
|
|
45
76
|
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
|
46
77
|
onGlobalFilterChange?: OnChangeFn<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Called instead of `onPaginationChange` when `clampPageIndex` moves a page
|
|
80
|
+
* index that pointed past the end, so a caller storing the page can replace
|
|
81
|
+
* the address that was wrong rather than pushing the correction on top of it.
|
|
82
|
+
*/
|
|
83
|
+
onPageIndexCorrected?: (pageIndex: number) => void;
|
|
47
84
|
onPaginationChange?: OnChangeFn<PaginationState>;
|
|
48
85
|
onRowClick?: (row: Row<TData>) => void;
|
|
49
86
|
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
|
|
50
87
|
onSortingChange?: OnChangeFn<SortingState>;
|
|
51
88
|
pageCount?: number;
|
|
52
89
|
pageSizeOptions?: number[];
|
|
90
|
+
paginationVariant?: DataTablePaginationVariant;
|
|
53
91
|
rowCount?: number;
|
|
54
92
|
state?: DataTableState;
|
|
55
93
|
tableOptions?: Partial<TableOptions<TData>>;
|
|
56
94
|
toolbar?: React.ReactNode;
|
|
57
95
|
};
|
|
58
|
-
type ResolvedDataTableLabels = Required<Pick<DataTableLabels, "columns" | "filterPlaceholder" | "nextPage" | "previousPage">> & DataTableLabels;
|
|
96
|
+
type ResolvedDataTableLabels = Required<Pick<DataTableLabels, "columns" | "filterPlaceholder" | "firstPage" | "lastPage" | "nextPage" | "previousPage" | "rowsPerPage" | "rowsPerPageName">> & DataTableLabels;
|
|
59
97
|
declare function TanStackDataTable<TData, TValue = unknown>(props: TanStackDataTableProps<TData, TValue>): React.JSX.Element;
|
|
60
98
|
export { TanStackDataTable };
|
|
61
|
-
export type { DataTableFilterMode, DataTableLabels, ResolvedDataTableLabels, TanStackDataTableProps, };
|
|
99
|
+
export type { DataTableFilterMode, DataTableLabels, DataTablePaginationVariant, DataTableState, ResolvedDataTableLabels, TanStackDataTableProps, };
|
|
@@ -4,17 +4,23 @@ import { getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFiltere
|
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
5
|
import { TableSkeleton } from "./loading-state";
|
|
6
6
|
import { DataTableTools } from "./data-table-controls";
|
|
7
|
+
import { DataTableProvider } from "./data-table-context";
|
|
7
8
|
import { DataTableGrid } from "./data-table-grid";
|
|
8
9
|
import { DataTablePagination } from "./data-table-pagination";
|
|
9
10
|
const defaultLabels = {
|
|
10
11
|
columns: "Columns",
|
|
11
12
|
empty: "No results.",
|
|
12
13
|
filterPlaceholder: "Filter rows...",
|
|
14
|
+
firstPage: "First page",
|
|
15
|
+
lastPage: "Last page",
|
|
13
16
|
nextPage: "Next page",
|
|
14
17
|
previousPage: "Previous page",
|
|
18
|
+
rowsPerPage: "Show",
|
|
19
|
+
rowsPerPageName: "Rows per page",
|
|
15
20
|
};
|
|
16
21
|
function TanStackDataTable(props) {
|
|
17
22
|
const table = useDataTableInstance(props);
|
|
23
|
+
useClampedPageIndex(table, props.clampPageIndex, props.loading, props.onPageIndexCorrected);
|
|
18
24
|
const labels = { ...defaultLabels, ...props.labels };
|
|
19
25
|
const rows = table.getRowModel().rows;
|
|
20
26
|
const pagination = table.getState().pagination;
|
|
@@ -23,12 +29,52 @@ function TanStackDataTable(props) {
|
|
|
23
29
|
props.actions ||
|
|
24
30
|
props.filterMode !== false ||
|
|
25
31
|
props.enableColumnVisibility !== false);
|
|
26
|
-
return (_jsxs("div", { "data-slot": "data-table", className: cn("
|
|
32
|
+
return (_jsx(DataTableProvider, { table: table, children: _jsxs("div", { "data-slot": "data-table", className: cn("flex flex-col gap-(--panel-gap)", props.className), children: [hasToolbar ? _jsx(DataTableTools, { labels: labels, props: props, table: table }) : null, _jsx("div", { "data-slot": "data-table-content", className: "min-h-0 flex-1 overflow-auto border-t border-border", children: props.loading ? (_jsx(TableSkeleton, { className: "rounded-none border-x-0 border-b-0", columns: table.getVisibleLeafColumns().length, rows: pagination.pageSize })) : (_jsx(DataTableGrid, { empty: props.empty ?? labels.empty, getRowClassName: props.getRowClassName, onRowClick: props.onRowClick, rows: rows, table: table })) }), props.enablePagination !== false ? (_jsx(DataTablePagination, { footerControls: props.footerControls, labels: labels, pageSizeOptions: props.pageSizeOptions ?? [10, 25, 50, 100], selectedRows: table.getFilteredSelectedRowModel().rows.length, table: table, totalRows: totalRows, variant: props.paginationVariant })) : null] }) }));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Move a page index that points past the last page back onto a page that
|
|
36
|
+
* exists, reporting the correction through `onPaginationChange`.
|
|
37
|
+
*
|
|
38
|
+
* A caller that keeps the page in the URL can be handed any number at all --
|
|
39
|
+
* `?page=99` in a bookmark, or a page that was valid until a filter narrowed
|
|
40
|
+
* the rows under it. Without this the grid renders an empty body while the
|
|
41
|
+
* footer still counts rows the reader cannot reach, and nothing puts the
|
|
42
|
+
* address right. Correcting through the caller's own handler rather than
|
|
43
|
+
* quietly inside the table keeps the URL and the table saying one thing.
|
|
44
|
+
*
|
|
45
|
+
* Off by default, and skipped while `loading`: it reaches out through the
|
|
46
|
+
* caller's handler, which for a caller that navigates on pagination is a
|
|
47
|
+
* navigation -- not something to start doing to an existing table uninvited,
|
|
48
|
+
* or to do at all while an empty result still means "not yet".
|
|
49
|
+
*
|
|
50
|
+
* `onPageIndexCorrected` exists so that handler can tell a correction from a
|
|
51
|
+
* page turn. A caller writing the page into the URL wants a turn to be a place
|
|
52
|
+
* to come back to and a correction to replace the address that was wrong --
|
|
53
|
+
* and without this they arrive as the same call.
|
|
54
|
+
*/
|
|
55
|
+
function useClampedPageIndex(table, enabled, loading, onCorrected) {
|
|
56
|
+
const { pageIndex } = table.getState().pagination;
|
|
57
|
+
const lastPageIndex = Math.max(table.getPageCount() - 1, 0);
|
|
58
|
+
const outOfRange = enabled === true && loading !== true && pageIndex > lastPageIndex;
|
|
59
|
+
const correctedRef = React.useRef(onCorrected);
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
correctedRef.current = onCorrected;
|
|
62
|
+
}, [onCorrected]);
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (!outOfRange)
|
|
65
|
+
return;
|
|
66
|
+
const corrected = correctedRef.current;
|
|
67
|
+
if (corrected)
|
|
68
|
+
corrected(lastPageIndex);
|
|
69
|
+
else
|
|
70
|
+
table.setPageIndex(lastPageIndex);
|
|
71
|
+
}, [lastPageIndex, outOfRange, table]);
|
|
27
72
|
}
|
|
28
73
|
// eslint-disable-next-line complexity -- TanStack table setup is declarative option wiring.
|
|
29
74
|
function useDataTableInstance(props) {
|
|
30
75
|
const [sorting, setSorting] = React.useState(props.state?.sorting ?? []);
|
|
31
76
|
const [columnFilters, setColumnFilters] = React.useState(props.state?.columnFilters ?? []);
|
|
77
|
+
const [columnOrder, setColumnOrder] = React.useState(props.state?.columnOrder ?? props.defaultColumnOrder ?? []);
|
|
32
78
|
const [columnVisibility, setColumnVisibility] = React.useState(props.state?.columnVisibility ?? props.defaultColumnVisibility ?? {});
|
|
33
79
|
const [rowSelection, setRowSelection] = React.useState(props.state?.rowSelection ?? {});
|
|
34
80
|
const [globalFilter, setGlobalFilter] = React.useState(props.state?.globalFilter ?? "");
|
|
@@ -37,12 +83,20 @@ function useDataTableInstance(props) {
|
|
|
37
83
|
return useReactTable({
|
|
38
84
|
data: props.data,
|
|
39
85
|
columns: props.columns,
|
|
86
|
+
// What `table.resetColumnOrder()` and `resetColumnVisibility()` restore.
|
|
87
|
+
// Seeding the hooks above is not enough: reset reads `initialState`, so
|
|
88
|
+
// without this a Reset control would clear a caller's defaults rather
|
|
89
|
+
// than return to them.
|
|
90
|
+
initialState: {
|
|
91
|
+
columnOrder: props.defaultColumnOrder ?? [],
|
|
92
|
+
columnVisibility: props.defaultColumnVisibility ?? {},
|
|
93
|
+
},
|
|
40
94
|
getRowId: props.getRowId,
|
|
41
95
|
pageCount: props.pageCount,
|
|
42
96
|
rowCount: props.rowCount,
|
|
43
97
|
enableColumnFilters: props.enableColumnFilters ?? true,
|
|
44
98
|
enableGlobalFilter: props.enableGlobalFilter ?? true,
|
|
45
|
-
enableHiding: props.enableColumnVisibility ?? true,
|
|
99
|
+
enableHiding: props.enableHiding ?? props.enableColumnVisibility ?? true,
|
|
46
100
|
enableRowSelection: props.enableRowSelection ?? false,
|
|
47
101
|
enableSorting: props.enableSorting ?? true,
|
|
48
102
|
manualFiltering: props.manualFiltering ?? false,
|
|
@@ -50,6 +104,7 @@ function useDataTableInstance(props) {
|
|
|
50
104
|
manualSorting: props.manualSorting ?? false,
|
|
51
105
|
state: {
|
|
52
106
|
columnFilters: props.state?.columnFilters ?? columnFilters,
|
|
107
|
+
columnOrder: props.state?.columnOrder ?? columnOrder,
|
|
53
108
|
columnVisibility: props.state?.columnVisibility ?? columnVisibility,
|
|
54
109
|
globalFilter: props.state?.globalFilter ?? globalFilter,
|
|
55
110
|
pagination: props.state?.pagination ?? pagination,
|
|
@@ -57,6 +112,7 @@ function useDataTableInstance(props) {
|
|
|
57
112
|
sorting: props.state?.sorting ?? sorting,
|
|
58
113
|
},
|
|
59
114
|
onColumnFiltersChange: props.onColumnFiltersChange ?? setColumnFilters,
|
|
115
|
+
onColumnOrderChange: props.onColumnOrderChange ?? setColumnOrder,
|
|
60
116
|
onColumnVisibilityChange: props.onColumnVisibilityChange ?? setColumnVisibility,
|
|
61
117
|
onGlobalFilterChange: props.onGlobalFilterChange ?? setGlobalFilter,
|
|
62
118
|
onPaginationChange: props.onPaginationChange ?? setPagination,
|
|
@@ -22,6 +22,10 @@ function DataTableSkeleton({ className, ...props }) {
|
|
|
22
22
|
}
|
|
23
23
|
function SortableTableHead({ active = false, children, className, direction = "asc", onSort, align = "left", ...props }) {
|
|
24
24
|
const Icon = active ? (direction === "asc" ? ArrowUpIcon : ArrowDownIcon) : ArrowUpDownIcon;
|
|
25
|
-
return (_jsx(TableHead, { "aria-sort": active ? (direction === "asc" ? "ascending" : "descending") : "none", className: cn(align === "right" && "text-right", className), ...props, children: _jsxs(Button, { "aria-pressed": active, className: cn(
|
|
25
|
+
return (_jsx(TableHead, { "aria-sort": active ? (direction === "asc" ? "ascending" : "descending") : "none", className: cn(align === "right" && "text-right", className), ...props, children: _jsxs(Button, { "aria-pressed": active, className: cn(
|
|
26
|
+
// `TableHead`'s size and weight, restated rather than inherited --
|
|
27
|
+
// Tailwind has no font-size or font-weight `inherit` utility -- so the
|
|
28
|
+
// two data tables in this package draw their header rows the same way.
|
|
29
|
+
"h-auto px-0 py-0 text-sm font-normal text-muted-foreground hover:bg-transparent hover:text-foreground", align === "right" ? "ml-auto justify-end" : "justify-start"), onClick: onSort, size: "sm", type: "button", variant: "ghost", children: [_jsx("span", { children: children }), _jsx(Icon, {})] }) }));
|
|
26
30
|
}
|
|
27
31
|
export { DataTable, DataTableToolbar, DataTableContent, DataTableEmpty, DataTableSkeleton, TanStackDataTable, SortableTableHead, };
|
|
@@ -53,7 +53,12 @@ function SidebarGap({ variant }) {
|
|
|
53
53
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)") }));
|
|
54
54
|
}
|
|
55
55
|
function SidebarInset({ className, ...props }) {
|
|
56
|
-
return (_jsx("main", { "data-slot": "sidebar-inset", className: cn(
|
|
56
|
+
return (_jsx("main", { "data-slot": "sidebar-inset", className: cn(
|
|
57
|
+
// `min-w-0` because this is a flex item beside the sidebar, and a flex
|
|
58
|
+
// item will not shrink below its content: without it a page holding
|
|
59
|
+
// anything wide -- a table with sized columns -- pushes the whole app
|
|
60
|
+
// sideways instead of scrolling inside its own box.
|
|
61
|
+
"relative flex w-full min-w-0 flex-1 flex-col bg-background", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-lg md:peer-data-[variant=inset]:shadow-xs md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", className), ...props }));
|
|
57
62
|
}
|
|
58
63
|
function SidebarInput({ className, ...props }) {
|
|
59
64
|
return (_jsx(Input, { "data-slot": "sidebar-input", "data-sidebar": "input", className: cn("h-8 w-full bg-background shadow-none", className), ...props }));
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
declare function Table({ className, ...props }: React.ComponentProps<"table">
|
|
2
|
+
declare function Table({ className, scrollable, ...props }: React.ComponentProps<"table"> & {
|
|
3
|
+
/**
|
|
4
|
+
* Whether the wrapper scrolls a too-wide table sideways. A caller that
|
|
5
|
+
* already scrolls the table from outside passes `false`: an `overflow` of
|
|
6
|
+
* anything but `visible` makes this wrapper a scrollport on *both* axes, so
|
|
7
|
+
* leaving it on would anchor a sticky header here rather than to the box
|
|
8
|
+
* that actually scrolls. Two overflow utilities cannot settle this between
|
|
9
|
+
* them -- `cn` does not dedupe them, and the axis-specific one wins -- so it
|
|
10
|
+
* is a switch rather than a class.
|
|
11
|
+
*/
|
|
12
|
+
scrollable?: boolean;
|
|
13
|
+
}): React.JSX.Element;
|
|
3
14
|
declare function TableHeader({ className, ...props }: React.ComponentProps<"thead">): React.JSX.Element;
|
|
4
15
|
declare function TableBody({ className, ...props }: React.ComponentProps<"tbody">): React.JSX.Element;
|
|
5
16
|
declare function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">): React.JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "../../lib/utils";
|
|
4
|
-
function Table({ className, ...props }) {
|
|
5
|
-
return (_jsx("div", { "data-slot": "table-container", className: "relative w-full overflow-x-auto", children: _jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }) }));
|
|
4
|
+
function Table({ className, scrollable = true, ...props }) {
|
|
5
|
+
return (_jsx("div", { "data-slot": "table-container", className: cn("relative w-full", scrollable && "overflow-x-auto"), children: _jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }) }));
|
|
6
6
|
}
|
|
7
7
|
function TableHeader({ className, ...props }) {
|
|
8
8
|
return _jsx("thead", { "data-slot": "table-header", className: cn("[&_tr]:border-b", className), ...props });
|
|
@@ -17,7 +17,12 @@ function TableRow({ className, ...props }) {
|
|
|
17
17
|
return (_jsx("tr", { "data-slot": "table-row", className: cn("h-(--table-row-height) border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted", className), ...props }));
|
|
18
18
|
}
|
|
19
19
|
function TableHead({ className, ...props }) {
|
|
20
|
-
return (_jsx("th", { "data-slot": "table-head", className: cn(
|
|
20
|
+
return (_jsx("th", { "data-slot": "table-head", className: cn(
|
|
21
|
+
// A column name is a name, not metadata, so it is set at the table's
|
|
22
|
+
// own size. It lives here rather than on the sort button so that a
|
|
23
|
+
// table mixing sortable and plain columns draws one header row and
|
|
24
|
+
// not two typefaces side by side.
|
|
25
|
+
"h-(--table-row-height) px-(--table-cell-padding-x) py-(--table-cell-padding-y) text-left align-middle text-sm font-normal tracking-normal whitespace-nowrap text-table-header-foreground normal-case [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className), ...props }));
|
|
21
26
|
}
|
|
22
27
|
function TableCell({ className, ...props }) {
|
|
23
28
|
return (_jsx("td", { "data-slot": "table-cell", className: cn("px-(--table-cell-padding-x) py-(--table-cell-padding-y) align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className), ...props }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-column-settings";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-column-settings";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-context";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-context";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-facet-filter";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-facet-filter";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-state";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-state";
|