@jielga/tmdatagrid 0.1.0 → 0.2.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/README.md +35 -2
- package/dist/index.d.ts +442 -337
- package/dist/index.js +811 -694
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +15 -7
- package/skills/columns/SKILL.md +8 -7
- package/skills/features/SKILL.md +104 -16
- package/skills/getting-started/SKILL.md +39 -16
- package/skills/options/SKILL.md +12 -7
- package/skills/server-side/SKILL.md +5 -2
- package/src/tmdatagrid/TMDataGridContext.ts +1 -1
- package/src/tmdatagrid/components/TMDataGrid.module.css +63 -0
- package/src/tmdatagrid/{TMDataGrid.tsx → components/TMDataGrid.tsx} +9 -4
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.module.css +25 -0
- package/src/tmdatagrid/{TMDataGridColumnsPanel.tsx → components/TMDataGridColumnsPanel.tsx} +3 -3
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +20 -0
- package/src/tmdatagrid/{TMDataGridFilterPanel.tsx → components/TMDataGridFilterPanel.tsx} +4 -4
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +10 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +143 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +153 -0
- package/src/tmdatagrid/{TMDataGridHeaderCell.tsx → components/TMDataGridHeaderCell.tsx} +22 -13
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +100 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +103 -0
- package/src/tmdatagrid/{TMDataGridTable.tsx → components/TMDataGridTable.tsx} +79 -20
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +12 -0
- package/src/tmdatagrid/{TMDataGridToolbar.tsx → components/TMDataGridToolbar.tsx} +5 -5
- package/src/tmdatagrid/components/sticky.module.css +45 -0
- package/src/tmdatagrid/{capabilities.ts → core/capabilities.ts} +30 -2
- package/src/tmdatagrid/{columnOrdering.ts → core/columnOrdering.ts} +2 -2
- package/src/tmdatagrid/{columnUtils.ts → core/columnUtils.ts} +1 -1
- package/src/tmdatagrid/{persistence.ts → core/persistence.ts} +80 -2
- package/src/tmdatagrid/index.ts +15 -9
- package/src/tmdatagrid/useTMDataGrid.tsx +102 -55
- package/src/tmdatagrid/TMDataGrid.module.css +0 -410
- package/src/tmdatagrid/TMDataGridFooter.tsx +0 -74
- /package/src/tmdatagrid/{icons.ts → components/icons.ts} +0 -0
- /package/src/tmdatagrid/{filterOperators.ts → core/filterOperators.ts} +0 -0
- /package/src/tmdatagrid/{sizes.ts → core/sizes.ts} +0 -0
|
@@ -7,14 +7,15 @@ import {
|
|
|
7
7
|
} from "@tanstack/react-table";
|
|
8
8
|
import { useSelector } from "@tanstack/react-store";
|
|
9
9
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
10
|
-
import { useCallback, useRef } from "react";
|
|
11
|
-
import classes from "./
|
|
12
|
-
import
|
|
10
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
11
|
+
import classes from "./TMDataGridTable.module.css";
|
|
12
|
+
import sticky from "./sticky.module.css";
|
|
13
|
+
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
13
14
|
import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
|
|
14
15
|
import { TMDataGridHeaderCell } from "./TMDataGridHeaderCell";
|
|
15
|
-
import { getColumnAlign } from "
|
|
16
|
+
import { getColumnAlign } from "../core/columnUtils";
|
|
16
17
|
import { SearchIcon } from "./icons";
|
|
17
|
-
import type { TMDataGridFeatures } from "
|
|
18
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
18
19
|
|
|
19
20
|
/** Where a column sits relative to the pinned regions, resolved once per render. */
|
|
20
21
|
export type TMDataGridColumnLayout = {
|
|
@@ -46,9 +47,9 @@ function TMDataGridBodyCell({
|
|
|
46
47
|
data-align={getColumnAlign(cell.column)}
|
|
47
48
|
className={[
|
|
48
49
|
classes.bodyCell,
|
|
49
|
-
layout.isBoundary && layout.pinnedAt === "left" ?
|
|
50
|
+
layout.isBoundary && layout.pinnedAt === "left" ? sticky.stickyLeft : "",
|
|
50
51
|
layout.isBoundary && layout.pinnedAt === "right"
|
|
51
|
-
?
|
|
52
|
+
? sticky.stickyRight
|
|
52
53
|
: "",
|
|
53
54
|
]
|
|
54
55
|
.filter(Boolean)
|
|
@@ -69,18 +70,23 @@ function TMDataGridBodyCell({
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export type TMDataGridTableProps<TData extends RowData> = {
|
|
72
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Called when a body row is clicked. Runs in addition to row selection under
|
|
75
|
+
* `rowSelectionMode: "row"`, not instead of it.
|
|
76
|
+
*/
|
|
73
77
|
onRowClick?: (row: Row<TMDataGridFeatures, TData>) => void;
|
|
74
78
|
};
|
|
75
79
|
|
|
76
80
|
/**
|
|
77
81
|
* The scrollable grid surface. Always virtualized: only the rows inside the
|
|
78
|
-
* viewport (plus overscan) are mounted
|
|
82
|
+
* viewport (plus overscan) are mounted — which is what makes the default
|
|
83
|
+
* no-pagination mode viable at any row count. Pagination is opt-in via
|
|
84
|
+
* `enablePagination` (or implied by `manualPagination`).
|
|
79
85
|
*/
|
|
80
86
|
export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
81
87
|
onRowClick,
|
|
82
88
|
}: TMDataGridTableProps<TData>) {
|
|
83
|
-
const { table, rowHeight, controlSize } = useTMDataGridContext();
|
|
89
|
+
const { table, features, rowHeight, controlSize } = useTMDataGridContext();
|
|
84
90
|
|
|
85
91
|
// The body depends on every state slice (sorting, filters, paging, sizing,
|
|
86
92
|
// visibility, selection), so it subscribes to the whole table store.
|
|
@@ -90,8 +96,23 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
90
96
|
table.options.meta ?? {};
|
|
91
97
|
|
|
92
98
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
93
|
-
//
|
|
94
|
-
|
|
99
|
+
// Pagination off (the default): every filtered+sorted row, virtualized.
|
|
100
|
+
// Pagination on: the current page; under `manualPagination` TanStack returns
|
|
101
|
+
// the rows as delivered, so the same branch serves server paging.
|
|
102
|
+
const rows = features.pagination
|
|
103
|
+
? table.getPaginatedRowModel().rows
|
|
104
|
+
: table.getPrePaginatedRowModel().rows;
|
|
105
|
+
|
|
106
|
+
// A persisted pageIndex can outlive the data that produced it; TanStack only
|
|
107
|
+
// auto-resets on live filter/sort/data changes, not on restored state. The
|
|
108
|
+
// guard makes the dependency-free effect idempotent.
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!features.pagination) return;
|
|
111
|
+
const pageCount = table.getPageCount();
|
|
112
|
+
if (pageCount > 0 && table.store.state.pagination.pageIndex >= pageCount) {
|
|
113
|
+
table.setPageIndex(pageCount - 1);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
95
116
|
|
|
96
117
|
const virtualizer = useVirtualizer({
|
|
97
118
|
count: rows.length,
|
|
@@ -200,17 +221,38 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
200
221
|
|
|
201
222
|
const isEmpty = rows.length === 0;
|
|
202
223
|
|
|
224
|
+
// "row" mode: the row itself is the selection control — clicking it toggles
|
|
225
|
+
// that row and leaves the rest of the selection alone.
|
|
226
|
+
const selectsOnRowClick =
|
|
227
|
+
features.rowSelection && features.rowSelectionMode === "row";
|
|
228
|
+
const rowIsInteractive = selectsOnRowClick || onRowClick !== undefined;
|
|
229
|
+
|
|
230
|
+
const handleRowActivate = (row: Row<TMDataGridFeatures, TMDataGridRowData>) => {
|
|
231
|
+
if (selectsOnRowClick && row.getCanSelect()) row.toggleSelected();
|
|
232
|
+
onRowClick?.(row as unknown as Row<TMDataGridFeatures, TData>);
|
|
233
|
+
};
|
|
234
|
+
|
|
203
235
|
return (
|
|
204
236
|
<div className={classes.tableWrapper}>
|
|
205
237
|
<div ref={scrollContainerRef} className={classes.scrollContainer}>
|
|
206
238
|
<div
|
|
207
239
|
role="table"
|
|
208
240
|
className={classes.grid}
|
|
241
|
+
// Virtualization means most rows are not in the DOM, so the counts
|
|
242
|
+
// have to be stated rather than inferred from what is rendered. Every
|
|
243
|
+
// row carries its aria-rowindex for the same reason.
|
|
244
|
+
aria-rowcount={rows.length + headerGroups.length}
|
|
245
|
+
aria-colcount={orderedColumns.length}
|
|
209
246
|
style={{ gridTemplateColumns, minWidth: gridMinWidth }}
|
|
210
247
|
>
|
|
211
248
|
<div role="rowgroup" style={{ display: "contents" }}>
|
|
212
|
-
{headerGroups.map((headerGroup) => (
|
|
213
|
-
<div
|
|
249
|
+
{headerGroups.map((headerGroup, groupIndex) => (
|
|
250
|
+
<div
|
|
251
|
+
key={headerGroup.id}
|
|
252
|
+
role="row"
|
|
253
|
+
aria-rowindex={groupIndex + 1}
|
|
254
|
+
className={classes.headerRow}
|
|
255
|
+
>
|
|
214
256
|
{headerGroup.headers.map((header) => (
|
|
215
257
|
<TMDataGridHeaderCell
|
|
216
258
|
key={header.id}
|
|
@@ -234,19 +276,36 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
234
276
|
<div
|
|
235
277
|
key={virtualItem.key}
|
|
236
278
|
role="row"
|
|
279
|
+
aria-rowindex={virtualItem.index + headerGroups.length + 1}
|
|
237
280
|
data-testid={`dg-row-${row.id}`}
|
|
238
281
|
data-selected={row.getIsSelected()}
|
|
282
|
+
// Selection is state; the highlight is a display choice, so
|
|
283
|
+
// the two are separate attributes.
|
|
284
|
+
data-highlighted={
|
|
285
|
+
features.highlightSelectedRows && row.getIsSelected()
|
|
286
|
+
}
|
|
287
|
+
data-selects-on-click={selectsOnRowClick}
|
|
288
|
+
aria-selected={
|
|
289
|
+
features.rowSelection ? row.getIsSelected() : undefined
|
|
290
|
+
}
|
|
239
291
|
className={classes.bodyRow}
|
|
240
292
|
style={{
|
|
241
293
|
minHeight: rowHeight,
|
|
242
|
-
cursor:
|
|
294
|
+
cursor: rowIsInteractive ? "pointer" : undefined,
|
|
243
295
|
}}
|
|
296
|
+
// Keyboard parity with the checkbox the row replaces.
|
|
297
|
+
tabIndex={selectsOnRowClick ? 0 : undefined}
|
|
244
298
|
onClick={
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
299
|
+
rowIsInteractive ? () => handleRowActivate(row) : undefined
|
|
300
|
+
}
|
|
301
|
+
onKeyDown={
|
|
302
|
+
selectsOnRowClick
|
|
303
|
+
? (event) => {
|
|
304
|
+
if (event.key !== " " && event.key !== "Enter") return;
|
|
305
|
+
if (event.target !== event.currentTarget) return;
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
handleRowActivate(row);
|
|
308
|
+
}
|
|
250
309
|
: undefined
|
|
251
310
|
}
|
|
252
311
|
>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { ActionIcon, Popover, Text, Tooltip } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import classes from "./
|
|
4
|
+
import classes from "./TMDataGridToolbar.module.css";
|
|
5
5
|
import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
|
|
6
|
-
import { useTMDataGridContext } from "
|
|
7
|
-
import { getGridCapabilities } from "
|
|
8
|
-
import { isFilterActive } from "
|
|
6
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
7
|
+
import { getGridCapabilities } from "../core/capabilities";
|
|
8
|
+
import { isFilterActive } from "../core/filterOperators";
|
|
9
9
|
import { BurgerIcon, FilterIcon } from "./icons";
|
|
10
|
-
import { openColumnFilter } from "
|
|
10
|
+
import { openColumnFilter } from "../useTMDataGrid";
|
|
11
11
|
|
|
12
12
|
/** Row above the grid. Compose it from the pieces below, or anything else. */
|
|
13
13
|
export function TMDataGridToolbar({ children }: { children?: ReactNode }) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Boundary of a pinned column group. Shared, because the edge is cast by both
|
|
2
|
+
the header cell and every body cell in the same column — the two have to
|
|
3
|
+
draw an identical band or the seam shows at the header.
|
|
4
|
+
|
|
5
|
+
A box-shadow is drawn per cell, and its blur fades in at each cell's own top
|
|
6
|
+
and bottom edge — stacked down a column that reads as banding at every row
|
|
7
|
+
seam. Instead: a crisp 1px border for the hard edge, plus a gradient pseudo
|
|
8
|
+
element pinned to `top: 0; bottom: 0` so consecutive cells butt together into
|
|
9
|
+
one unbroken band. Mantine sets `color-scheme` on the root, so light-dark()
|
|
10
|
+
resolves without a second selector. */
|
|
11
|
+
.stickyLeft {
|
|
12
|
+
border-right: 1px solid var(--mantine-color-default-border);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.stickyRight {
|
|
16
|
+
border-left: 1px solid var(--mantine-color-default-border);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.stickyLeft::after,
|
|
20
|
+
.stickyRight::after {
|
|
21
|
+
content: "";
|
|
22
|
+
position: absolute;
|
|
23
|
+
top: 0;
|
|
24
|
+
bottom: 0;
|
|
25
|
+
width: 4px;
|
|
26
|
+
pointer-events: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.stickyLeft::after {
|
|
30
|
+
left: 100%;
|
|
31
|
+
background: linear-gradient(
|
|
32
|
+
to right,
|
|
33
|
+
light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)),
|
|
34
|
+
transparent
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.stickyRight::after {
|
|
39
|
+
right: 100%;
|
|
40
|
+
background: linear-gradient(
|
|
41
|
+
to left,
|
|
42
|
+
light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)),
|
|
43
|
+
transparent
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Column, RowData } from "@tanstack/react-table";
|
|
2
|
-
import type { TMDataGridRowData } from "
|
|
2
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
3
|
import { isColumnReorderable } from "./columnUtils";
|
|
4
4
|
import type {
|
|
5
5
|
TMDataGridFeatures,
|
|
6
6
|
TMDataGridTable,
|
|
7
7
|
UseTMDataGridOptions,
|
|
8
|
-
} from "
|
|
8
|
+
} from "../useTMDataGrid";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Table-level feature switches, read straight off the options object.
|
|
@@ -19,6 +19,11 @@ import type {
|
|
|
19
19
|
* TanStack still decides: these flags are combined with `getCanX()`, which is
|
|
20
20
|
* what applies the per-column overrides.
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* How rows are selected — see `rowSelectionMode` on {@link UseTMDataGridOptions}.
|
|
24
|
+
*/
|
|
25
|
+
export type TMDataGridRowSelectionMode = "checkbox" | "row";
|
|
26
|
+
|
|
22
27
|
export type TMDataGridFeatureFlags = {
|
|
23
28
|
sorting: boolean;
|
|
24
29
|
filtering: boolean;
|
|
@@ -27,6 +32,16 @@ export type TMDataGridFeatureFlags = {
|
|
|
27
32
|
resizing: boolean;
|
|
28
33
|
ordering: boolean;
|
|
29
34
|
rowSelection: boolean;
|
|
35
|
+
/** Only meaningful while `rowSelection` is on. Defaults to `"checkbox"`. */
|
|
36
|
+
rowSelectionMode: TMDataGridRowSelectionMode;
|
|
37
|
+
/** Whether a selected row takes the highlight colour. Follows the mode. */
|
|
38
|
+
highlightSelectedRows: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The one default-off flag: pagination must be asked for, either with the
|
|
41
|
+
* grid's `enablePagination` or implicitly by declaring `manualPagination`.
|
|
42
|
+
* Off, the grid renders every filtered row and relies on virtualization.
|
|
43
|
+
*/
|
|
44
|
+
pagination: boolean;
|
|
30
45
|
};
|
|
31
46
|
|
|
32
47
|
export function readFeatureFlags<TData extends RowData>(
|
|
@@ -39,6 +54,10 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
39
54
|
| "enableColumnResizing"
|
|
40
55
|
| "enableColumnOrdering"
|
|
41
56
|
| "enableRowSelection"
|
|
57
|
+
| "rowSelectionMode"
|
|
58
|
+
| "highlightSelectedRows"
|
|
59
|
+
| "enablePagination"
|
|
60
|
+
| "manualPagination"
|
|
42
61
|
>,
|
|
43
62
|
): TMDataGridFeatureFlags {
|
|
44
63
|
return {
|
|
@@ -49,6 +68,13 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
49
68
|
resizing: options.enableColumnResizing !== false,
|
|
50
69
|
ordering: options.enableColumnOrdering !== false,
|
|
51
70
|
rowSelection: options.enableRowSelection !== false,
|
|
71
|
+
rowSelectionMode: options.rowSelectionMode ?? "checkbox",
|
|
72
|
+
// In "row" mode the highlight *is* the feedback for a click, so it is on.
|
|
73
|
+
// With checkboxes the box already says it, so it is off unless asked for.
|
|
74
|
+
highlightSelectedRows:
|
|
75
|
+
options.highlightSelectedRows ?? options.rowSelectionMode === "row",
|
|
76
|
+
pagination:
|
|
77
|
+
options.enablePagination === true || options.manualPagination === true,
|
|
52
78
|
};
|
|
53
79
|
}
|
|
54
80
|
|
|
@@ -96,6 +122,7 @@ export type TMDataGridCapabilities = {
|
|
|
96
122
|
canPinAny: boolean;
|
|
97
123
|
canReorderAny: boolean;
|
|
98
124
|
canSelectRows: boolean;
|
|
125
|
+
canPaginate: boolean;
|
|
99
126
|
};
|
|
100
127
|
|
|
101
128
|
export function getGridCapabilities(
|
|
@@ -113,5 +140,6 @@ export function getGridCapabilities(
|
|
|
113
140
|
canPinAny: any((c) => c.canPin),
|
|
114
141
|
canReorderAny: any((c) => c.canReorder),
|
|
115
142
|
canSelectRows: features.rowSelection,
|
|
143
|
+
canPaginate: features.pagination,
|
|
116
144
|
};
|
|
117
145
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Column, ColumnPinningState } from "@tanstack/react-table";
|
|
2
|
-
import type { TMDataGridRowData } from "
|
|
2
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
3
|
import { isColumnReorderable } from "./columnUtils";
|
|
4
|
-
import type { TMDataGridFeatures, TMDataGridTable } from "
|
|
4
|
+
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
5
5
|
|
|
6
6
|
type GridColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
7
7
|
type GridTable = TMDataGridTable<TMDataGridRowData>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TMDataGridColumnType } from "./filterOperators";
|
|
2
|
-
import type { TMDataGridColumnMeta } from "
|
|
2
|
+
import type { TMDataGridColumnMeta } from "../useTMDataGrid";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Structural shape of the column bits the chrome reads. Kept minimal so these
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TableState } from "@tanstack/react-table";
|
|
2
|
-
import type { TMDataGridFeatures } from "
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
3
|
|
|
4
4
|
type GridState = TableState<TMDataGridFeatures>;
|
|
5
5
|
|
|
@@ -100,6 +100,82 @@ function resolveStorage(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Shape guards for restored state.
|
|
105
|
+
*
|
|
106
|
+
* A stored payload outlives the code that wrote it: a slice can change shape
|
|
107
|
+
* between releases, and nothing stops anyone from editing storage by hand. The
|
|
108
|
+
* value goes straight into `initialState`, where a wrong shape surfaces as a
|
|
109
|
+
* crash inside TanStack rather than as a bad read here. A slice that fails its
|
|
110
|
+
* guard is dropped; the rest still restore.
|
|
111
|
+
*/
|
|
112
|
+
function prop(value: unknown, key: string): unknown {
|
|
113
|
+
return typeof value === "object" && value !== null
|
|
114
|
+
? (value as Record<string, unknown>)[key]
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isString = (value: unknown): value is string => typeof value === "string";
|
|
119
|
+
|
|
120
|
+
const isFiniteNumber = (value: unknown): value is number =>
|
|
121
|
+
typeof value === "number" && Number.isFinite(value);
|
|
122
|
+
|
|
123
|
+
function isArrayOf(
|
|
124
|
+
value: unknown,
|
|
125
|
+
isItem: (item: unknown) => boolean,
|
|
126
|
+
): boolean {
|
|
127
|
+
return Array.isArray(value) && value.every(isItem);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isRecordOf(
|
|
131
|
+
value: unknown,
|
|
132
|
+
isItem: (item: unknown) => boolean,
|
|
133
|
+
): boolean {
|
|
134
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return Object.values(value).every(isItem);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hasStringId = (entry: unknown): boolean => isString(prop(entry, "id"));
|
|
141
|
+
|
|
142
|
+
const SLICE_GUARDS: Record<
|
|
143
|
+
TMDataGridDataSlice | TMDataGridSettingsSlice,
|
|
144
|
+
(value: unknown) => boolean
|
|
145
|
+
> = {
|
|
146
|
+
columnFilters: (value) => isArrayOf(value, hasStringId),
|
|
147
|
+
// TanStack leaves the global filter value untyped — the grid's own default
|
|
148
|
+
// filters on a string, but a consumer may store anything here.
|
|
149
|
+
globalFilter: () => true,
|
|
150
|
+
sorting: (value) =>
|
|
151
|
+
isArrayOf(
|
|
152
|
+
value,
|
|
153
|
+
(entry) => hasStringId(entry) && typeof prop(entry, "desc") === "boolean",
|
|
154
|
+
),
|
|
155
|
+
pagination: (value) => {
|
|
156
|
+
const pageIndex = prop(value, "pageIndex");
|
|
157
|
+
const pageSize = prop(value, "pageSize");
|
|
158
|
+
return (
|
|
159
|
+
isFiniteNumber(pageIndex) &&
|
|
160
|
+
pageIndex >= 0 &&
|
|
161
|
+
isFiniteNumber(pageSize) &&
|
|
162
|
+
pageSize > 0
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
columnVisibility: (value) =>
|
|
166
|
+
isRecordOf(value, (entry) => typeof entry === "boolean"),
|
|
167
|
+
columnSizing: (value) => isRecordOf(value, isFiniteNumber),
|
|
168
|
+
columnOrder: (value) => isArrayOf(value, isString),
|
|
169
|
+
columnPinning: (value) =>
|
|
170
|
+
isArrayOf(prop(value, "left"), isString) &&
|
|
171
|
+
isArrayOf(prop(value, "right"), isString),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function isValidSlice(slice: keyof GridState, value: unknown): boolean {
|
|
175
|
+
const guard = SLICE_GUARDS[slice as keyof typeof SLICE_GUARDS];
|
|
176
|
+
return guard === undefined || guard(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
103
179
|
function readSlices(
|
|
104
180
|
storage: Storage | null,
|
|
105
181
|
resolved: ResolvedKey | null,
|
|
@@ -117,7 +193,9 @@ function readSlices(
|
|
|
117
193
|
// Only copy the selected slices. A payload written before the selection was
|
|
118
194
|
// narrowed cannot reintroduce slices the caller has since opted out of.
|
|
119
195
|
for (const slice of resolved.slices) {
|
|
120
|
-
if (slice in record)
|
|
196
|
+
if (!(slice in record)) continue;
|
|
197
|
+
if (!isValidSlice(slice, record[slice])) continue;
|
|
198
|
+
restored[slice] = record[slice];
|
|
121
199
|
}
|
|
122
200
|
return restored as Partial<GridState>;
|
|
123
201
|
} catch {
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export { TMDataGrid, type TMDataGridProps } from "./TMDataGrid";
|
|
1
|
+
export { TMDataGrid, type TMDataGridProps } from "./components/TMDataGrid";
|
|
2
2
|
export {
|
|
3
3
|
useTMDataGridContext,
|
|
4
4
|
type TMDataGridContextValue,
|
|
5
5
|
type TMDataGridRowData,
|
|
6
6
|
} from "./TMDataGridContext";
|
|
7
|
+
export { SELECT_COLUMN_ID } from "./components/TMDataGridSelectColumn";
|
|
7
8
|
export {
|
|
8
9
|
createTMDataGridColumnHelper,
|
|
9
10
|
openColumnFilter,
|
|
10
|
-
SELECT_COLUMN_ID,
|
|
11
11
|
tmDataGridFeatures,
|
|
12
12
|
type TMDataGridApi,
|
|
13
13
|
type TMDataGridColumnMeta,
|
|
@@ -26,12 +26,12 @@ export {
|
|
|
26
26
|
type TMDataGridFilterValue,
|
|
27
27
|
FILTER_OPERATOR_LABELS,
|
|
28
28
|
isFilterActive,
|
|
29
|
-
} from "./filterOperators";
|
|
29
|
+
} from "./core/filterOperators";
|
|
30
30
|
export {
|
|
31
31
|
getColumnLabel,
|
|
32
32
|
getColumnType,
|
|
33
33
|
isColumnReorderable,
|
|
34
|
-
} from "./columnUtils";
|
|
34
|
+
} from "./core/columnUtils";
|
|
35
35
|
export {
|
|
36
36
|
getColumnRegion,
|
|
37
37
|
getStepTargetColumn,
|
|
@@ -41,7 +41,7 @@ export {
|
|
|
41
41
|
type MoveColumnArgs,
|
|
42
42
|
type TMDataGridColumnRegion,
|
|
43
43
|
type TMDataGridDropSide,
|
|
44
|
-
} from "./columnOrdering";
|
|
44
|
+
} from "./core/columnOrdering";
|
|
45
45
|
export {
|
|
46
46
|
getColumnCapabilities,
|
|
47
47
|
getGridCapabilities,
|
|
@@ -49,13 +49,14 @@ export {
|
|
|
49
49
|
type TMDataGridCapabilities,
|
|
50
50
|
type TMDataGridColumnCapabilities,
|
|
51
51
|
type TMDataGridFeatureFlags,
|
|
52
|
-
|
|
52
|
+
type TMDataGridRowSelectionMode,
|
|
53
|
+
} from "./core/capabilities";
|
|
53
54
|
export {
|
|
54
55
|
DEFAULT_TMDATAGRID_SIZE,
|
|
55
56
|
SIZE_CONTROL_SIZE,
|
|
56
57
|
SIZE_ROW_HEIGHT,
|
|
57
58
|
type TMDataGridSize,
|
|
58
|
-
} from "./sizes";
|
|
59
|
+
} from "./core/sizes";
|
|
59
60
|
export {
|
|
60
61
|
DATA_STATE_SLICES,
|
|
61
62
|
SETTINGS_STATE_SLICES,
|
|
@@ -64,5 +65,10 @@ export {
|
|
|
64
65
|
type TMDataGridPersistKey,
|
|
65
66
|
type TMDataGridSettingsSlice,
|
|
66
67
|
type TMDataGridStorageMode,
|
|
67
|
-
} from "./persistence";
|
|
68
|
-
export type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
68
|
+
} from "./core/persistence";
|
|
69
|
+
export type { TMDataGridColumnLayout } from "./components/TMDataGridTable";
|
|
70
|
+
export {
|
|
71
|
+
getTMDataGridPaginationApi,
|
|
72
|
+
type TMDataGridFooterProps,
|
|
73
|
+
type TMDataGridPaginationApi,
|
|
74
|
+
} from "./components/TMDataGridFooter";
|