@jielga/tmdatagrid 0.1.0 → 0.3.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 +792 -327
- package/dist/index.js +1581 -892
- 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 +78 -20
- 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 +73 -0
- package/src/tmdatagrid/{TMDataGrid.tsx → components/TMDataGrid.tsx} +15 -4
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.module.css +25 -0
- package/src/tmdatagrid/{TMDataGridColumnsPanel.tsx → components/TMDataGridColumnsPanel.tsx} +10 -4
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +35 -0
- package/src/tmdatagrid/{TMDataGridFilterPanel.tsx → components/TMDataGridFilterPanel.tsx} +91 -14
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +135 -0
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +18 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +171 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +147 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +159 -0
- package/src/tmdatagrid/{TMDataGridHeaderCell.tsx → components/TMDataGridHeaderCell.tsx} +99 -14
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +177 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +137 -0
- package/src/tmdatagrid/components/TMDataGridTable.tsx +720 -0
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +12 -0
- package/src/tmdatagrid/{TMDataGridToolbar.tsx → components/TMDataGridToolbar.tsx} +8 -5
- package/src/tmdatagrid/{icons.ts → components/icons.ts} +5 -0
- package/src/tmdatagrid/components/sticky.module.css +45 -0
- package/src/tmdatagrid/core/capabilities.ts +215 -0
- package/src/tmdatagrid/{columnOrdering.ts → core/columnOrdering.ts} +2 -2
- package/src/tmdatagrid/{columnUtils.ts → core/columnUtils.ts} +1 -1
- package/src/tmdatagrid/{filterOperators.ts → core/filterOperators.ts} +25 -0
- package/src/tmdatagrid/core/grouping.ts +21 -0
- package/src/tmdatagrid/{persistence.ts → core/persistence.ts} +92 -2
- package/src/tmdatagrid/core/rowSelection.ts +202 -0
- package/src/tmdatagrid/index.ts +39 -9
- package/src/tmdatagrid/useTMDataGrid.tsx +318 -66
- package/src/tmdatagrid/TMDataGrid.module.css +0 -410
- package/src/tmdatagrid/TMDataGridFooter.tsx +0 -74
- package/src/tmdatagrid/TMDataGridTable.tsx +0 -297
- package/src/tmdatagrid/capabilities.ts +0 -117
- /package/src/tmdatagrid/{sizes.ts → core/sizes.ts} +0 -0
|
@@ -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 }) {
|
|
@@ -112,6 +112,9 @@ export function TMDataGridFilterButton() {
|
|
|
112
112
|
color={activeCount > 0 ? undefined : "gray"}
|
|
113
113
|
size={controlSize}
|
|
114
114
|
aria-label="Filters"
|
|
115
|
+
// Marks this as the panel's own toggle, so the panel's click-away
|
|
116
|
+
// handler leaves it alone and the button stays a toggle.
|
|
117
|
+
data-dg-filter-toggle
|
|
115
118
|
onClick={() => {
|
|
116
119
|
if (opened) {
|
|
117
120
|
ui.actions.closeFilterPanel();
|
|
@@ -10,12 +10,17 @@ export {
|
|
|
10
10
|
IconArrowLeft as MoveLeftIcon,
|
|
11
11
|
IconArrowRight as MoveRightIcon,
|
|
12
12
|
IconArrowUp as ArrowUpIcon,
|
|
13
|
+
IconChevronDown as ChevronDownIcon,
|
|
13
14
|
IconChevronLeft as ChevronLeftIcon,
|
|
14
15
|
IconChevronRight as ChevronRightIcon,
|
|
15
16
|
IconColumns3 as ColumnsIcon,
|
|
16
17
|
IconDotsVertical as DotsVerticalIcon,
|
|
17
18
|
IconEyeOff as EyeOffIcon,
|
|
18
19
|
IconFilter as FilterIcon,
|
|
20
|
+
IconFoldDown as ExpandAllIcon,
|
|
21
|
+
IconFoldUp as CollapseAllIcon,
|
|
22
|
+
IconIndentDecrease as UngroupIcon,
|
|
23
|
+
IconIndentIncrease as GroupIcon,
|
|
19
24
|
IconArrowBarToLeft as PinLeftIcon,
|
|
20
25
|
IconArrowBarToRight as PinRightIcon,
|
|
21
26
|
IconMenu2 as BurgerIcon,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { Column, RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
|
+
import { isColumnReorderable } from "./columnUtils";
|
|
4
|
+
import type {
|
|
5
|
+
TMDataGridFeatures,
|
|
6
|
+
TMDataGridTable,
|
|
7
|
+
UseTMDataGridOptions,
|
|
8
|
+
} from "../useTMDataGrid";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Table-level feature switches, read straight off the options object.
|
|
12
|
+
*
|
|
13
|
+
* Why not just call `column.getCanSort()` everywhere? Because that is a method
|
|
14
|
+
* call on a `column` whose identity survives an options change, so the React
|
|
15
|
+
* Compiler caches the result and the chrome keeps rendering for a feature that
|
|
16
|
+
* was switched off. Reading the options into a fresh object each render gives
|
|
17
|
+
* every derived value something that actually changes to invalidate on.
|
|
18
|
+
*
|
|
19
|
+
* TanStack still decides: these flags are combined with `getCanX()`, which is
|
|
20
|
+
* what applies the per-column overrides.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* How rows are selected — see `selectionMode` on {@link UseTMDataGridOptions}.
|
|
24
|
+
*
|
|
25
|
+
* One axis, because the two things a config could set independently — which
|
|
26
|
+
* chrome selects, and what a bare row click does — cannot both be free: a click
|
|
27
|
+
* can either toggle a multi-selection or move the highlight, never both. Folding
|
|
28
|
+
* them into one value makes that conflict unrepresentable rather than something
|
|
29
|
+
* to warn about.
|
|
30
|
+
*/
|
|
31
|
+
export type TMDataGridSelectionMode =
|
|
32
|
+
/** Checkbox column, multi-select. Clicking a row elsewhere does nothing. */
|
|
33
|
+
| "checkbox"
|
|
34
|
+
/** No checkbox column; row click multi-selects, with Ctrl/Shift modifiers. */
|
|
35
|
+
| "row"
|
|
36
|
+
/** Checkbox column multi-selects, and a row click highlights one row. */
|
|
37
|
+
| "checkboxAndHighlight"
|
|
38
|
+
/** No selection at all — a row click only highlights. Master-detail. */
|
|
39
|
+
| "highlight";
|
|
40
|
+
|
|
41
|
+
export type TMDataGridFeatureFlags = {
|
|
42
|
+
sorting: boolean;
|
|
43
|
+
filtering: boolean;
|
|
44
|
+
hiding: boolean;
|
|
45
|
+
pinning: boolean;
|
|
46
|
+
resizing: boolean;
|
|
47
|
+
ordering: boolean;
|
|
48
|
+
/** Whether any row selection is possible. Off outright under `"highlight"`. */
|
|
49
|
+
rowSelection: boolean;
|
|
50
|
+
/** Defaults to `"checkbox"`. */
|
|
51
|
+
selectionMode: TMDataGridSelectionMode;
|
|
52
|
+
/** Whether the generated checkbox column is prepended. */
|
|
53
|
+
selectColumn: boolean;
|
|
54
|
+
/** Whether a bare row click toggles the selection. Only under `"row"`. */
|
|
55
|
+
rowClickSelects: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether more than one row can be selected at once — TanStack's
|
|
58
|
+
* `enableMultiRowSelection`. Off, the grid drops the select-all header
|
|
59
|
+
* checkbox: TanStack's `toggleAllRowsSelected` only consults `getCanSelect`,
|
|
60
|
+
* so that control would select every row and walk straight past the limit.
|
|
61
|
+
*
|
|
62
|
+
* A per-row predicate counts as on, since some rows may still multi-select.
|
|
63
|
+
*/
|
|
64
|
+
multiRowSelection: boolean;
|
|
65
|
+
/** Whether a selected row takes the highlight colour. Follows the mode. */
|
|
66
|
+
showSelectedBackground: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The single highlighted row — clicking a row highlights it, for a detail
|
|
69
|
+
* panel to follow. State of its own, not a slice of `rowSelection`, which is
|
|
70
|
+
* what lets it coexist with a checkbox multi-selection.
|
|
71
|
+
*/
|
|
72
|
+
highlightRow: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The one default-off flag: pagination must be asked for, either with the
|
|
75
|
+
* grid's `enablePagination` or implicitly by declaring `manualPagination`.
|
|
76
|
+
* Off, the grid renders every filtered row and relies on virtualization.
|
|
77
|
+
*/
|
|
78
|
+
pagination: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Row grouping: the "Group by" items in the header menu and the generated
|
|
81
|
+
* tree column. On unless `enableGrouping: false`, since an ungrouped grid
|
|
82
|
+
* looks and behaves exactly as it did before — `grouping` starts empty, and
|
|
83
|
+
* an empty grouping state passes straight through the row model.
|
|
84
|
+
*
|
|
85
|
+
* The exception is `manualPagination`, where the client holds one page rather
|
|
86
|
+
* than the whole set: grouping that page would build groups out of an
|
|
87
|
+
* arbitrary slice and quietly show wrong counts. A server-side grid that does
|
|
88
|
+
* its own grouping can still say `enableGrouping: true` to override.
|
|
89
|
+
*/
|
|
90
|
+
grouping: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function readFeatureFlags<TData extends RowData>(
|
|
94
|
+
options: Pick<
|
|
95
|
+
UseTMDataGridOptions<TData>,
|
|
96
|
+
| "enableSorting"
|
|
97
|
+
| "enableColumnFilters"
|
|
98
|
+
| "enableHiding"
|
|
99
|
+
| "enableColumnPinning"
|
|
100
|
+
| "enableColumnResizing"
|
|
101
|
+
| "enableColumnOrdering"
|
|
102
|
+
| "enableRowSelection"
|
|
103
|
+
| "enableMultiRowSelection"
|
|
104
|
+
| "selectionMode"
|
|
105
|
+
| "showSelectedBackground"
|
|
106
|
+
| "enablePagination"
|
|
107
|
+
| "manualPagination"
|
|
108
|
+
| "enableGrouping"
|
|
109
|
+
>,
|
|
110
|
+
): TMDataGridFeatureFlags {
|
|
111
|
+
const selectionMode = options.selectionMode ?? "checkbox";
|
|
112
|
+
// `"highlight"` is the master-detail mode: no selection to speak of, so the
|
|
113
|
+
// checkbox column and the click-to-select behaviour both fall away. Otherwise
|
|
114
|
+
// `enableRowSelection` still has the final say, including its predicate form.
|
|
115
|
+
const rowSelection =
|
|
116
|
+
selectionMode !== "highlight" && options.enableRowSelection !== false;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
sorting: options.enableSorting !== false,
|
|
120
|
+
filtering: options.enableColumnFilters !== false,
|
|
121
|
+
hiding: options.enableHiding !== false,
|
|
122
|
+
pinning: options.enableColumnPinning !== false,
|
|
123
|
+
resizing: options.enableColumnResizing !== false,
|
|
124
|
+
ordering: options.enableColumnOrdering !== false,
|
|
125
|
+
rowSelection,
|
|
126
|
+
selectionMode,
|
|
127
|
+
selectColumn:
|
|
128
|
+
rowSelection &&
|
|
129
|
+
(selectionMode === "checkbox" || selectionMode === "checkboxAndHighlight"),
|
|
130
|
+
rowClickSelects: rowSelection && selectionMode === "row",
|
|
131
|
+
multiRowSelection: options.enableMultiRowSelection !== false,
|
|
132
|
+
// In "row" mode the highlight *is* the feedback for a click, so it is on.
|
|
133
|
+
// With checkboxes the box already says it, so it is off unless asked for.
|
|
134
|
+
showSelectedBackground:
|
|
135
|
+
options.showSelectedBackground ?? selectionMode === "row",
|
|
136
|
+
highlightRow:
|
|
137
|
+
selectionMode === "checkboxAndHighlight" || selectionMode === "highlight",
|
|
138
|
+
pagination:
|
|
139
|
+
options.enablePagination === true || options.manualPagination === true,
|
|
140
|
+
grouping: options.enableGrouping ?? options.manualPagination !== true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What one column's header may offer.
|
|
146
|
+
*
|
|
147
|
+
* | Affordance | Turned off by |
|
|
148
|
+
* | ---------- | ------------- |
|
|
149
|
+
* | Sort arrow, Sort ASC/DESC | `enableSorting` (table or column) |
|
|
150
|
+
* | Filter item, panel entry | `enableColumnFilters` / `enableColumnFilter` |
|
|
151
|
+
* | Hide column | `enableHiding` (table or column) |
|
|
152
|
+
* | Pin to left / right | `enableColumnPinning` / `enablePinning` |
|
|
153
|
+
* | Resize dragging | `enableColumnResizing` / `enableResizing` |
|
|
154
|
+
* | Header dragging, Move left / right | `enableColumnOrdering` / `meta.enableOrdering` |
|
|
155
|
+
* | Group by / Ungroup | `enableGrouping` (table or column) |
|
|
156
|
+
*/
|
|
157
|
+
export type TMDataGridColumnCapabilities = {
|
|
158
|
+
canSort: boolean;
|
|
159
|
+
canFilter: boolean;
|
|
160
|
+
canHide: boolean;
|
|
161
|
+
canPin: boolean;
|
|
162
|
+
canResize: boolean;
|
|
163
|
+
canReorder: boolean;
|
|
164
|
+
canGroup: boolean;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export function getColumnCapabilities(
|
|
168
|
+
column: Column<TMDataGridFeatures, TMDataGridRowData, unknown>,
|
|
169
|
+
features: TMDataGridFeatureFlags,
|
|
170
|
+
): TMDataGridColumnCapabilities {
|
|
171
|
+
return {
|
|
172
|
+
canSort: features.sorting && column.getCanSort(),
|
|
173
|
+
canFilter: features.filtering && column.getCanFilter(),
|
|
174
|
+
canHide: features.hiding && column.getCanHide(),
|
|
175
|
+
canPin: features.pinning && column.getCanPin(),
|
|
176
|
+
canResize: features.resizing && column.getCanResize(),
|
|
177
|
+
// Ordering has no TanStack capability method — see isColumnReorderable.
|
|
178
|
+
canReorder: features.ordering && isColumnReorderable(column),
|
|
179
|
+
// `getCanGroup()` also insists on an accessor, which is what keeps the
|
|
180
|
+
// generated checkbox and tree columns from offering to group on themselves.
|
|
181
|
+
canGroup: features.grouping && column.getCanGroup(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The same questions, asked of the whole grid — used to hide toolbar buttons. */
|
|
186
|
+
export type TMDataGridCapabilities = {
|
|
187
|
+
canSortAny: boolean;
|
|
188
|
+
canFilterAny: boolean;
|
|
189
|
+
canHideAny: boolean;
|
|
190
|
+
canPinAny: boolean;
|
|
191
|
+
canReorderAny: boolean;
|
|
192
|
+
canGroupAny: boolean;
|
|
193
|
+
canSelectRows: boolean;
|
|
194
|
+
canPaginate: boolean;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export function getGridCapabilities(
|
|
198
|
+
table: TMDataGridTable<TMDataGridRowData>,
|
|
199
|
+
features: TMDataGridFeatureFlags,
|
|
200
|
+
): TMDataGridCapabilities {
|
|
201
|
+
const columns = table.getAllLeafColumns();
|
|
202
|
+
const any = (predicate: (capabilities: TMDataGridColumnCapabilities) => boolean) =>
|
|
203
|
+
columns.some((column) => predicate(getColumnCapabilities(column, features)));
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
canSortAny: any((c) => c.canSort),
|
|
207
|
+
canFilterAny: any((c) => c.canFilter),
|
|
208
|
+
canHideAny: any((c) => c.canHide),
|
|
209
|
+
canPinAny: any((c) => c.canPin),
|
|
210
|
+
canReorderAny: any((c) => c.canReorder),
|
|
211
|
+
canGroupAny: any((c) => c.canGroup),
|
|
212
|
+
canSelectRows: features.rowSelection,
|
|
213
|
+
canPaginate: features.pagination,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -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
|
|
@@ -106,6 +106,31 @@ export function isFilterActive(value: unknown): boolean {
|
|
|
106
106
|
return !operatorNeedsValue(value.operator) || value.value.trim() !== "";
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* One-line description of a single filter, as shown on a filter pill.
|
|
111
|
+
*
|
|
112
|
+
* The type's default operator is left implicit — "First name: Sofia" reads the
|
|
113
|
+
* way a person would say it — while any other operator is spelled out, since
|
|
114
|
+
* that is the part a reader cannot guess.
|
|
115
|
+
*/
|
|
116
|
+
export function formatFilterLabel({
|
|
117
|
+
label,
|
|
118
|
+
type,
|
|
119
|
+
filter,
|
|
120
|
+
}: {
|
|
121
|
+
label: string;
|
|
122
|
+
type: TMDataGridColumnType;
|
|
123
|
+
filter: TMDataGridFilterValue;
|
|
124
|
+
}): string {
|
|
125
|
+
const { operator, value } = filter;
|
|
126
|
+
if (!operatorNeedsValue(operator)) {
|
|
127
|
+
return `${label} ${FILTER_OPERATOR_LABELS[operator]}`;
|
|
128
|
+
}
|
|
129
|
+
const text = value.trim();
|
|
130
|
+
if (operator === getDefaultOperator(type)) return `${label}: ${text}`;
|
|
131
|
+
return `${label} ${FILTER_OPERATOR_LABELS[operator]} ${text}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
export function matchesFilter(
|
|
110
135
|
cellValue: unknown,
|
|
111
136
|
{ operator, value }: TMDataGridFilterValue,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Row, RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The data rows under a group row, at any depth.
|
|
6
|
+
*
|
|
7
|
+
* Not `row.getLeafRows()`, despite the name: that flattens the whole subtree
|
|
8
|
+
* and keeps the branches, so a group nested two deep reports its sub-groups
|
|
9
|
+
* alongside the records. Grouping `city` then `name` would have Stockholm
|
|
10
|
+
* counting four rows and three names as seven, and one tick on it selecting
|
|
11
|
+
* ids that hold no record.
|
|
12
|
+
*
|
|
13
|
+
* A row with no subRows is a data row, which also makes this the identity on an
|
|
14
|
+
* ungrouped grid.
|
|
15
|
+
*/
|
|
16
|
+
export function getGroupDataRows<TData extends RowData>(
|
|
17
|
+
row: Row<TMDataGridFeatures, TData>,
|
|
18
|
+
): Array<Row<TMDataGridFeatures, TData>> {
|
|
19
|
+
if (row.subRows.length === 0) return [row];
|
|
20
|
+
return row.getLeafRows().filter((leaf) => leaf.subRows.length === 0);
|
|
21
|
+
}
|
|
@@ -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
|
|
|
@@ -28,6 +28,10 @@ export const DATA_STATE_SLICES = [
|
|
|
28
28
|
"globalFilter",
|
|
29
29
|
"sorting",
|
|
30
30
|
"pagination",
|
|
31
|
+
// Which groups are open. Keyed by group ids built from the values themselves
|
|
32
|
+
// (`department:Design`), so a restored entry for a group the data no longer
|
|
33
|
+
// produces is simply never looked up.
|
|
34
|
+
"expanded",
|
|
31
35
|
] as const satisfies ReadonlyArray<keyof GridState>;
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -39,6 +43,10 @@ export const SETTINGS_STATE_SLICES = [
|
|
|
39
43
|
"columnSizing",
|
|
40
44
|
"columnOrder",
|
|
41
45
|
"columnPinning",
|
|
46
|
+
// Which columns the rows are grouped by. A settings slice rather than a data
|
|
47
|
+
// one: it names columns, not values, so it survives the data changing under
|
|
48
|
+
// it the way the column layout does.
|
|
49
|
+
"grouping",
|
|
42
50
|
] as const satisfies ReadonlyArray<keyof GridState>;
|
|
43
51
|
|
|
44
52
|
export type TMDataGridDataSlice = (typeof DATA_STATE_SLICES)[number];
|
|
@@ -100,6 +108,86 @@ function resolveStorage(
|
|
|
100
108
|
}
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Shape guards for restored state.
|
|
113
|
+
*
|
|
114
|
+
* A stored payload outlives the code that wrote it: a slice can change shape
|
|
115
|
+
* between releases, and nothing stops anyone from editing storage by hand. The
|
|
116
|
+
* value goes straight into `initialState`, where a wrong shape surfaces as a
|
|
117
|
+
* crash inside TanStack rather than as a bad read here. A slice that fails its
|
|
118
|
+
* guard is dropped; the rest still restore.
|
|
119
|
+
*/
|
|
120
|
+
function prop(value: unknown, key: string): unknown {
|
|
121
|
+
return typeof value === "object" && value !== null
|
|
122
|
+
? (value as Record<string, unknown>)[key]
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isString = (value: unknown): value is string => typeof value === "string";
|
|
127
|
+
|
|
128
|
+
const isFiniteNumber = (value: unknown): value is number =>
|
|
129
|
+
typeof value === "number" && Number.isFinite(value);
|
|
130
|
+
|
|
131
|
+
function isArrayOf(
|
|
132
|
+
value: unknown,
|
|
133
|
+
isItem: (item: unknown) => boolean,
|
|
134
|
+
): boolean {
|
|
135
|
+
return Array.isArray(value) && value.every(isItem);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isRecordOf(
|
|
139
|
+
value: unknown,
|
|
140
|
+
isItem: (item: unknown) => boolean,
|
|
141
|
+
): boolean {
|
|
142
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return Object.values(value).every(isItem);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hasStringId = (entry: unknown): boolean => isString(prop(entry, "id"));
|
|
149
|
+
|
|
150
|
+
const SLICE_GUARDS: Record<
|
|
151
|
+
TMDataGridDataSlice | TMDataGridSettingsSlice,
|
|
152
|
+
(value: unknown) => boolean
|
|
153
|
+
> = {
|
|
154
|
+
columnFilters: (value) => isArrayOf(value, hasStringId),
|
|
155
|
+
// TanStack leaves the global filter value untyped — the grid's own default
|
|
156
|
+
// filters on a string, but a consumer may store anything here.
|
|
157
|
+
globalFilter: () => true,
|
|
158
|
+
sorting: (value) =>
|
|
159
|
+
isArrayOf(
|
|
160
|
+
value,
|
|
161
|
+
(entry) => hasStringId(entry) && typeof prop(entry, "desc") === "boolean",
|
|
162
|
+
),
|
|
163
|
+
pagination: (value) => {
|
|
164
|
+
const pageIndex = prop(value, "pageIndex");
|
|
165
|
+
const pageSize = prop(value, "pageSize");
|
|
166
|
+
return (
|
|
167
|
+
isFiniteNumber(pageIndex) &&
|
|
168
|
+
pageIndex >= 0 &&
|
|
169
|
+
isFiniteNumber(pageSize) &&
|
|
170
|
+
pageSize > 0
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
// `true` is a legal ExpandedState of its own, meaning every row is open.
|
|
174
|
+
expanded: (value) =>
|
|
175
|
+
value === true || isRecordOf(value, (entry) => typeof entry === "boolean"),
|
|
176
|
+
columnVisibility: (value) =>
|
|
177
|
+
isRecordOf(value, (entry) => typeof entry === "boolean"),
|
|
178
|
+
columnSizing: (value) => isRecordOf(value, isFiniteNumber),
|
|
179
|
+
columnOrder: (value) => isArrayOf(value, isString),
|
|
180
|
+
columnPinning: (value) =>
|
|
181
|
+
isArrayOf(prop(value, "left"), isString) &&
|
|
182
|
+
isArrayOf(prop(value, "right"), isString),
|
|
183
|
+
grouping: (value) => isArrayOf(value, isString),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function isValidSlice(slice: keyof GridState, value: unknown): boolean {
|
|
187
|
+
const guard = SLICE_GUARDS[slice as keyof typeof SLICE_GUARDS];
|
|
188
|
+
return guard === undefined || guard(value);
|
|
189
|
+
}
|
|
190
|
+
|
|
103
191
|
function readSlices(
|
|
104
192
|
storage: Storage | null,
|
|
105
193
|
resolved: ResolvedKey | null,
|
|
@@ -117,7 +205,9 @@ function readSlices(
|
|
|
117
205
|
// Only copy the selected slices. A payload written before the selection was
|
|
118
206
|
// narrowed cannot reintroduce slices the caller has since opted out of.
|
|
119
207
|
for (const slice of resolved.slices) {
|
|
120
|
-
if (slice in record)
|
|
208
|
+
if (!(slice in record)) continue;
|
|
209
|
+
if (!isValidSlice(slice, record[slice])) continue;
|
|
210
|
+
restored[slice] = record[slice];
|
|
121
211
|
}
|
|
122
212
|
return restored as Partial<GridState>;
|
|
123
213
|
} catch {
|