@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,25 +1,29 @@
|
|
|
1
|
-
import { Checkbox } from "@mantine/core";
|
|
2
1
|
import { useCreateStore } from "@tanstack/react-store";
|
|
3
2
|
import {
|
|
3
|
+
aggregationFns,
|
|
4
4
|
type ColumnDef,
|
|
5
5
|
columnFacetingFeature,
|
|
6
6
|
columnFilteringFeature,
|
|
7
|
+
columnGroupingFeature,
|
|
7
8
|
columnOrderingFeature,
|
|
8
9
|
columnPinningFeature,
|
|
9
10
|
columnResizingFeature,
|
|
10
11
|
columnSizingFeature,
|
|
11
12
|
columnVisibilityFeature,
|
|
12
13
|
createColumnHelper,
|
|
14
|
+
createExpandedRowModel,
|
|
13
15
|
createFacetedMinMaxValues,
|
|
14
16
|
createFacetedRowModel,
|
|
15
17
|
createFacetedUniqueValues,
|
|
16
18
|
createFilteredRowModel,
|
|
19
|
+
createGroupedRowModel,
|
|
17
20
|
createPaginatedRowModel,
|
|
18
21
|
createSortedRowModel,
|
|
19
22
|
filterFns,
|
|
20
23
|
globalFilteringFeature,
|
|
21
24
|
metaHelper,
|
|
22
25
|
type RowData,
|
|
26
|
+
rowExpandingFeature,
|
|
23
27
|
rowPaginationFeature,
|
|
24
28
|
rowSelectionFeature,
|
|
25
29
|
rowSortingFeature,
|
|
@@ -27,24 +31,42 @@ import {
|
|
|
27
31
|
type Table,
|
|
28
32
|
tableFeatures,
|
|
29
33
|
type TableOptions,
|
|
34
|
+
type TableState,
|
|
30
35
|
useTable,
|
|
31
36
|
} from "@tanstack/react-table";
|
|
32
37
|
import type { Store } from "@tanstack/store";
|
|
33
|
-
import { useEffect, useMemo, useState } from "react";
|
|
38
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
34
39
|
import {
|
|
35
40
|
type TMDataGridColumnType,
|
|
36
41
|
tmDataGridFilterFn,
|
|
37
42
|
getDefaultOperator,
|
|
38
|
-
} from "./filterOperators";
|
|
43
|
+
} from "./core/filterOperators";
|
|
39
44
|
import {
|
|
40
45
|
hasPersistenceKeys,
|
|
41
46
|
readPersistedState,
|
|
42
47
|
type TMDataGridPersistence,
|
|
43
48
|
writePersistedState,
|
|
44
|
-
} from "./persistence";
|
|
45
|
-
import {
|
|
49
|
+
} from "./core/persistence";
|
|
50
|
+
import {
|
|
51
|
+
readFeatureFlags,
|
|
52
|
+
type TMDataGridFeatureFlags,
|
|
53
|
+
type TMDataGridSelectionMode,
|
|
54
|
+
} from "./core/capabilities";
|
|
55
|
+
import {
|
|
56
|
+
createSelectColumn,
|
|
57
|
+
SELECT_COLUMN_ID,
|
|
58
|
+
} from "./components/TMDataGridSelectColumn";
|
|
59
|
+
import {
|
|
60
|
+
createGroupColumn,
|
|
61
|
+
GROUP_COLUMN_ID,
|
|
62
|
+
} from "./components/TMDataGridGroupColumn";
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
/**
|
|
65
|
+
* How long the grid waits after the last state change before writing to
|
|
66
|
+
* storage. Long enough to collapse a resize drag into one write, short enough
|
|
67
|
+
* that a reload right after a change still sees it.
|
|
68
|
+
*/
|
|
69
|
+
const PERSIST_DEBOUNCE_MS = 200;
|
|
48
70
|
|
|
49
71
|
/** Per-column configuration the TMDataGrid chrome reads. */
|
|
50
72
|
export type TMDataGridColumnMeta = {
|
|
@@ -92,9 +114,16 @@ export const tmDataGridFeatures = tableFeatures({
|
|
|
92
114
|
columnSizingFeature,
|
|
93
115
|
columnResizingFeature,
|
|
94
116
|
columnFacetingFeature,
|
|
117
|
+
columnGroupingFeature,
|
|
118
|
+
// Registered for grouping's sake rather than for tree data: the grouped row
|
|
119
|
+
// model builds the parent rows, and this is what flattens the expanded ones
|
|
120
|
+
// back into the flat list the body virtualizes.
|
|
121
|
+
rowExpandingFeature,
|
|
95
122
|
|
|
96
123
|
filteredRowModel: createFilteredRowModel(),
|
|
124
|
+
groupedRowModel: createGroupedRowModel(),
|
|
97
125
|
sortedRowModel: createSortedRowModel(),
|
|
126
|
+
expandedRowModel: createExpandedRowModel(),
|
|
98
127
|
paginatedRowModel: createPaginatedRowModel(),
|
|
99
128
|
facetedRowModel: createFacetedRowModel(),
|
|
100
129
|
facetedMinMaxValues: createFacetedMinMaxValues(),
|
|
@@ -102,6 +131,10 @@ export const tmDataGridFeatures = tableFeatures({
|
|
|
102
131
|
|
|
103
132
|
filterFns: { ...filterFns, tmDataGrid: tmDataGridFilterFn },
|
|
104
133
|
sortFns,
|
|
134
|
+
// The names `columnDef.aggregationFn` accepts. Only consulted for columns
|
|
135
|
+
// that ask for one — a column with no `aggregationFn` reports `undefined` on
|
|
136
|
+
// a group row, which is what keeps a plain "group by" free of aggregates.
|
|
137
|
+
aggregationFns,
|
|
105
138
|
|
|
106
139
|
tableMeta: metaHelper<TMDataGridTableMeta>(),
|
|
107
140
|
columnMeta: metaHelper<TMDataGridColumnMeta>(),
|
|
@@ -133,6 +166,20 @@ export type TMDataGridUiState = {
|
|
|
133
166
|
* `dataTransfer`, which browsers keep unreadable until the drop.
|
|
134
167
|
*/
|
|
135
168
|
draggedColumnId: string | null;
|
|
169
|
+
/**
|
|
170
|
+
* The single highlighted row — the one a detail panel would be showing. Its
|
|
171
|
+
* own concept, not a slice of `rowSelection`: under
|
|
172
|
+
* `selectionMode: "checkboxAndHighlight"` the two coexist, and TanStack's one
|
|
173
|
+
* selection map cannot hold both.
|
|
174
|
+
*
|
|
175
|
+
* Not pruned when the row is filtered out, paged away or dropped from `data`,
|
|
176
|
+
* matching how TanStack treats `rowSelection` — nothing there resets it
|
|
177
|
+
* either. The row simply renders unhighlighted, and highlights again if it
|
|
178
|
+
* comes back.
|
|
179
|
+
*/
|
|
180
|
+
highlightedRowId: string | null;
|
|
181
|
+
/** The pivot a shift-click extends from. See resolveRowSelectionClick. */
|
|
182
|
+
selectionAnchorRowId: string | null;
|
|
136
183
|
};
|
|
137
184
|
|
|
138
185
|
export type TMDataGridUiActions = {
|
|
@@ -142,6 +189,12 @@ export type TMDataGridUiActions = {
|
|
|
142
189
|
toggleColumnsPanel: () => void;
|
|
143
190
|
startColumnDrag: (columnId: string) => void;
|
|
144
191
|
endColumnDrag: () => void;
|
|
192
|
+
/**
|
|
193
|
+
* Moves the active row, or clears it with `null` — which is how a consumer
|
|
194
|
+
* closing its detail panel puts the grid back in step.
|
|
195
|
+
*/
|
|
196
|
+
setHighlightedRow: (rowId: string | null) => void;
|
|
197
|
+
setSelectionAnchor: (rowId: string | null) => void;
|
|
145
198
|
};
|
|
146
199
|
|
|
147
200
|
export type TMDataGridUiStore = Store<TMDataGridUiState, TMDataGridUiActions>;
|
|
@@ -175,6 +228,79 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
175
228
|
* reordering is entirely a matter of interface.
|
|
176
229
|
*/
|
|
177
230
|
enableColumnOrdering?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Client-side pagination and the built-in `TMDataGrid.Footer` pager.
|
|
233
|
+
* Defaults to `false`: the grid renders every filtered row and relies on
|
|
234
|
+
* virtualization.
|
|
235
|
+
*
|
|
236
|
+
* The second grid-defined switch (TanStack defines no `enablePagination`
|
|
237
|
+
* option). `manualPagination: true` implies it — a server-paged grid needs
|
|
238
|
+
* no extra flag.
|
|
239
|
+
*/
|
|
240
|
+
enablePagination?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* How rows are selected. Defaults to `"checkbox"`.
|
|
243
|
+
*
|
|
244
|
+
* | Mode | Checkbox column | Row click |
|
|
245
|
+
* | ---- | --------------- | --------- |
|
|
246
|
+
* | `"checkbox"` | yes, multi-select | nothing |
|
|
247
|
+
* | `"row"` | no | multi-selects, Ctrl/Shift modifiers |
|
|
248
|
+
* | `"checkboxAndHighlight"` | yes, multi-select | highlights one row |
|
|
249
|
+
* | `"highlight"` | no | highlights one row, no selection at all |
|
|
250
|
+
*
|
|
251
|
+
* One option rather than two, so the combination that cannot work — a click
|
|
252
|
+
* that both toggles a multi-selection and moves the highlight — is not
|
|
253
|
+
* expressible.
|
|
254
|
+
*
|
|
255
|
+
* The first two write to TanStack's `rowSelection`. The highlight is separate
|
|
256
|
+
* state, which is what lets `"checkboxAndHighlight"` run both at once: tick
|
|
257
|
+
* rows for a bulk action, click one to open its detail panel. See
|
|
258
|
+
* `defaultHighlightedRowId` / `onHighlightedRowChange`.
|
|
259
|
+
*
|
|
260
|
+
* `enableRowSelection` still gates the selection half, predicate form
|
|
261
|
+
* included; under `"highlight"` there is nothing for it to gate.
|
|
262
|
+
*/
|
|
263
|
+
selectionMode?: TMDataGridSelectionMode;
|
|
264
|
+
/**
|
|
265
|
+
* Give selected rows the highlight background. Defaults to `true` under
|
|
266
|
+
* `selectionMode: "row"`, where the highlight is the only feedback a click
|
|
267
|
+
* gives, and `false` under `"checkbox"`, where the box already shows it.
|
|
268
|
+
*
|
|
269
|
+
* The colour is the `--dg-row-selected-bg` CSS variable, so it can be changed
|
|
270
|
+
* without turning the flag on or off:
|
|
271
|
+
*
|
|
272
|
+
* ```tsx
|
|
273
|
+
* <TMDataGrid
|
|
274
|
+
* {...grid}
|
|
275
|
+
* style={{ "--dg-row-selected-bg": "var(--mantine-color-blue-0)" }}
|
|
276
|
+
* />
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
showSelectedBackground?: boolean;
|
|
280
|
+
/**
|
|
281
|
+
* The row that starts out highlighted, under a `selectionMode` that has a
|
|
282
|
+
* highlight. Read once on mount, like `initialState`.
|
|
283
|
+
*
|
|
284
|
+
* The grid never persists the highlighted row. Pair this with
|
|
285
|
+
* {@link onHighlightedRowChange} and keep it wherever it belongs — for a
|
|
286
|
+
* detail panel that is usually the route, which gets you a shareable link and
|
|
287
|
+
* a working back button as well as surviving a reload:
|
|
288
|
+
*
|
|
289
|
+
* ```tsx
|
|
290
|
+
* const { rowId } = useParams();
|
|
291
|
+
* useTMDataGrid({
|
|
292
|
+
* selectionMode: "highlight",
|
|
293
|
+
* defaultHighlightedRowId: rowId,
|
|
294
|
+
* onHighlightedRowChange: (id) => navigate(id ? `/employees/${id}` : "/employees"),
|
|
295
|
+
* });
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
defaultHighlightedRowId?: string | null;
|
|
299
|
+
/**
|
|
300
|
+
* Called with the newly highlighted row id, or `null` when it is cleared.
|
|
301
|
+
* Fires for `ui.actions.setHighlightedRow` too, not only for clicks.
|
|
302
|
+
*/
|
|
303
|
+
onHighlightedRowChange?: (rowId: string | null) => void;
|
|
178
304
|
};
|
|
179
305
|
|
|
180
306
|
type TMDataGridColumnDef<TData extends RowData> = ColumnDef<
|
|
@@ -184,8 +310,17 @@ type TMDataGridColumnDef<TData extends RowData> = ColumnDef<
|
|
|
184
310
|
>;
|
|
185
311
|
|
|
186
312
|
/**
|
|
187
|
-
* Point every column at the operator-dispatching filter function
|
|
188
|
-
* column opted into its
|
|
313
|
+
* Point every column at the operator-dispatching filter function, and take
|
|
314
|
+
* grouping's aggregation defaults back off, unless the column opted into its
|
|
315
|
+
* own. Anything the consumer set wins — it is spread over these.
|
|
316
|
+
*
|
|
317
|
+
* The aggregation pair needs explaining. TanStack's grouping feature hands
|
|
318
|
+
* every column `aggregationFn: "auto"` and an `aggregatedCell` that stringifies
|
|
319
|
+
* whatever comes out, so merely registering the feature would have every
|
|
320
|
+
* numeric column silently sum itself and every date column show a range the
|
|
321
|
+
* moment anything is grouped. That is a summary table, and "group by" is not a
|
|
322
|
+
* request for one. Cleared here, so a grouped grid is a tree until a column
|
|
323
|
+
* says otherwise — `aggregationFn: "sum"` on the column that wants it.
|
|
189
324
|
*/
|
|
190
325
|
function withTMDataGridDefaults<TData extends RowData>(
|
|
191
326
|
columns: ReadonlyArray<TMDataGridColumnDef<TData>>,
|
|
@@ -197,52 +332,15 @@ function withTMDataGridDefaults<TData extends RowData>(
|
|
|
197
332
|
columns: withTMDataGridDefaults<TData>(column.columns),
|
|
198
333
|
};
|
|
199
334
|
}
|
|
200
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
filterFn: "tmDataGrid",
|
|
337
|
+
aggregationFn: undefined,
|
|
338
|
+
aggregatedCell: undefined,
|
|
339
|
+
...column,
|
|
340
|
+
} as TMDataGridColumnDef<TData>;
|
|
201
341
|
});
|
|
202
342
|
}
|
|
203
343
|
|
|
204
|
-
function createSelectColumn<TData extends RowData>(): TMDataGridColumnDef<TData> {
|
|
205
|
-
return {
|
|
206
|
-
id: SELECT_COLUMN_ID,
|
|
207
|
-
meta: {
|
|
208
|
-
label: "Checkbox selection",
|
|
209
|
-
align: "center",
|
|
210
|
-
// Structurally the first column; it also anchors the left pinned lane, so
|
|
211
|
-
// no other column can be moved in front of it.
|
|
212
|
-
enableOrdering: false,
|
|
213
|
-
},
|
|
214
|
-
size: 48,
|
|
215
|
-
minSize: 48,
|
|
216
|
-
maxSize: 48,
|
|
217
|
-
enableResizing: false,
|
|
218
|
-
enableSorting: false,
|
|
219
|
-
enableColumnFilter: false,
|
|
220
|
-
enableGlobalFilter: false,
|
|
221
|
-
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
222
|
-
enablePinning: false,
|
|
223
|
-
header: ({ table }) => (
|
|
224
|
-
<Checkbox
|
|
225
|
-
size="xs"
|
|
226
|
-
aria-label="Select all rows"
|
|
227
|
-
checked={table.getIsAllRowsSelected()}
|
|
228
|
-
indeterminate={table.getIsSomeRowsSelected()}
|
|
229
|
-
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
230
|
-
/>
|
|
231
|
-
),
|
|
232
|
-
cell: ({ row }) => (
|
|
233
|
-
<Checkbox
|
|
234
|
-
size="xs"
|
|
235
|
-
aria-label="Select row"
|
|
236
|
-
checked={row.getIsSelected()}
|
|
237
|
-
disabled={!row.getCanSelect()}
|
|
238
|
-
indeterminate={row.getIsSomeSelected()}
|
|
239
|
-
onChange={row.getToggleSelectedHandler()}
|
|
240
|
-
onClick={(event) => event.stopPropagation()}
|
|
241
|
-
/>
|
|
242
|
-
),
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
344
|
/**
|
|
247
345
|
* Builds a TMDataGrid table plus its chrome store.
|
|
248
346
|
*
|
|
@@ -250,49 +348,100 @@ function createSelectColumn<TData extends RowData>(): TMDataGridColumnDef<TData>
|
|
|
250
348
|
* only needs `manualPagination` / `manualFiltering` / `manualSorting`,
|
|
251
349
|
* `rowCount` and the matching `onXChange` callbacks — the chrome reads
|
|
252
350
|
* `getRowCount()` / `getPageCount()` / `getPaginatedRowModel()`, all of which
|
|
253
|
-
* already respect manual mode.
|
|
351
|
+
* already respect manual mode. `manualPagination` also switches the pagination
|
|
352
|
+
* flag on, so `<TMDataGrid.Footer />` renders its pager without further
|
|
353
|
+
* options.
|
|
254
354
|
*/
|
|
255
355
|
export function useTMDataGrid<TData extends RowData>({
|
|
256
356
|
persist,
|
|
257
|
-
// Not
|
|
357
|
+
// Not TanStack options, so they are kept out of what `useTable` receives.
|
|
258
358
|
enableColumnOrdering,
|
|
359
|
+
enablePagination,
|
|
360
|
+
selectionMode,
|
|
361
|
+
showSelectedBackground,
|
|
362
|
+
defaultHighlightedRowId,
|
|
363
|
+
onHighlightedRowChange,
|
|
259
364
|
...options
|
|
260
365
|
}: UseTMDataGridOptions<TData>): TMDataGridApi<TData> {
|
|
261
|
-
|
|
366
|
+
// Derived up here, rather than just before the return, because the rest of the
|
|
367
|
+
// hook needs `selectColumn` — one place decides what each mode means.
|
|
368
|
+
//
|
|
369
|
+
// Deliberately not memoized on `table`: the flags must re-derive whenever the
|
|
370
|
+
// caller passes different options, and `table` keeps the same identity when
|
|
371
|
+
// they do. See readFeatureFlags.
|
|
372
|
+
const features = readFeatureFlags({
|
|
373
|
+
...options,
|
|
374
|
+
enableColumnOrdering,
|
|
375
|
+
enablePagination,
|
|
376
|
+
selectionMode,
|
|
377
|
+
showSelectedBackground,
|
|
378
|
+
});
|
|
379
|
+
|
|
262
380
|
const pinningEnabled = options.enableColumnPinning !== false;
|
|
381
|
+
const selectColumnEnabled = features.selectColumn;
|
|
382
|
+
const groupColumnEnabled = features.grouping;
|
|
263
383
|
|
|
264
384
|
const columns = useMemo(() => {
|
|
265
385
|
const base = withTMDataGridDefaults<TData>(
|
|
266
386
|
options.columns as ReadonlyArray<TMDataGridColumnDef<TData>>,
|
|
267
387
|
);
|
|
268
|
-
|
|
269
|
-
|
|
388
|
+
// Both generated columns are always present when their feature is on, and
|
|
389
|
+
// the tree column hides itself while nothing is grouped. Adding it to the
|
|
390
|
+
// array only once a column is grouped would make the column list depend on
|
|
391
|
+
// table state, which is the one thing that cannot be a `useMemo` dependency
|
|
392
|
+
// here — the table is built from these columns.
|
|
393
|
+
return [
|
|
394
|
+
...(selectColumnEnabled ? [createSelectColumn<TData>()] : []),
|
|
395
|
+
...(groupColumnEnabled ? [createGroupColumn<TData>()] : []),
|
|
396
|
+
...base,
|
|
397
|
+
];
|
|
398
|
+
}, [options.columns, selectColumnEnabled, groupColumnEnabled]);
|
|
270
399
|
|
|
271
400
|
// Read once on mount: `initialState` is only consumed on the first render,
|
|
272
401
|
// and re-reading later would fight the user's live edits.
|
|
273
402
|
const [persistedState] = useState(() => readPersistedState(persist));
|
|
274
403
|
|
|
404
|
+
// Restored grouping decides whether the tree column starts out visible, so a
|
|
405
|
+
// reload comes back to the tree the user left rather than to a hidden lane.
|
|
406
|
+
const initialGrouping =
|
|
407
|
+
persistedState.grouping ?? options.initialState?.grouping ?? [];
|
|
408
|
+
|
|
275
409
|
const table = useTable({
|
|
276
410
|
columnResizeMode: "onChange",
|
|
277
411
|
enableSorting: true,
|
|
278
412
|
enableColumnResizing: true,
|
|
279
413
|
globalFilterFn: "includesString",
|
|
414
|
+
// Grouping by a column takes it out of the grid, the way AG Grid does it:
|
|
415
|
+
// its values have moved into the tree column, so leaving it in place would
|
|
416
|
+
// show every row the same value it was grouped under. Overridable — pass
|
|
417
|
+
// `"reorder"` to keep the column and have it moved to the front instead.
|
|
418
|
+
groupedColumnMode: "remove",
|
|
280
419
|
...options,
|
|
281
420
|
features: tmDataGridFeatures,
|
|
282
421
|
columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
|
|
283
422
|
initialState: {
|
|
284
423
|
...options.initialState,
|
|
285
424
|
...persistedState,
|
|
425
|
+
columnVisibility: {
|
|
426
|
+
...options.initialState?.columnVisibility,
|
|
427
|
+
...persistedState.columnVisibility,
|
|
428
|
+
// Last word, because the tree column's visibility is not a user setting
|
|
429
|
+
// — it tracks the grouping state. See the effect below.
|
|
430
|
+
...(groupColumnEnabled
|
|
431
|
+
? { [GROUP_COLUMN_ID]: initialGrouping.length > 0 }
|
|
432
|
+
: {}),
|
|
433
|
+
},
|
|
286
434
|
columnPinning: {
|
|
287
|
-
// The
|
|
288
|
-
// top of anything restored from storage.
|
|
435
|
+
// The generated columns are structurally pinned, so they are re-applied
|
|
436
|
+
// on top of anything restored from storage.
|
|
289
437
|
left: [
|
|
290
|
-
...(
|
|
438
|
+
...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
439
|
+
...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
|
|
291
440
|
...(
|
|
292
441
|
persistedState.columnPinning?.left ??
|
|
293
442
|
options.initialState?.columnPinning?.left ??
|
|
294
443
|
[]
|
|
295
|
-
).filter((id) => id !== SELECT_COLUMN_ID),
|
|
444
|
+
).filter((id) => id !== SELECT_COLUMN_ID && id !== GROUP_COLUMN_ID),
|
|
296
445
|
],
|
|
297
446
|
right:
|
|
298
447
|
persistedState.columnPinning?.right ??
|
|
@@ -308,16 +457,94 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
308
457
|
},
|
|
309
458
|
});
|
|
310
459
|
|
|
460
|
+
// Two things have to happen whenever `grouping` changes.
|
|
461
|
+
//
|
|
462
|
+
// One: the tree column appears with the first grouped column and goes away
|
|
463
|
+
// with the last, so an ungrouped grid looks exactly as it did before grouping
|
|
464
|
+
// existed. Driven from a subscription rather than by rebuilding the column
|
|
465
|
+
// array, because the array is what the table is built from — deriving it from
|
|
466
|
+
// table state would close the loop. Visibility is the one column property
|
|
467
|
+
// that can be changed after the fact without touching the definitions.
|
|
468
|
+
//
|
|
469
|
+
// Two, and this one is a workaround. In table-core 9.0.0-beta.21 the
|
|
470
|
+
// per-region column APIs do not list `grouping` among their memo
|
|
471
|
+
// dependencies, even though they all derive from `getAllLeafColumns()`, which
|
|
472
|
+
// does:
|
|
473
|
+
//
|
|
474
|
+
// | API | Declares |
|
|
475
|
+
// | --- | --- |
|
|
476
|
+
// | `getLeft/Center/RightVisibleLeafColumns` | columns, columnPinning, columnVisibility, columnOrder |
|
|
477
|
+
// | `getLeft/Center/RightHeaderGroups` | columnPinning, columnOrder |
|
|
478
|
+
// | `row.getLeft/Center/RightVisibleCells` | columnPinning, columnVisibility |
|
|
479
|
+
//
|
|
480
|
+
// So grouping a *second* column leaves every one of them returning the
|
|
481
|
+
// previous list: the column TanStack removed keeps its header and its grid
|
|
482
|
+
// track, and the row cells no longer line up with them. The first grouping
|
|
483
|
+
// appears to work only because the visibility write above happens to touch a
|
|
484
|
+
// dependency they share.
|
|
485
|
+
//
|
|
486
|
+
// Re-publishing `columnVisibility` and `columnOrder` — same contents, new
|
|
487
|
+
// identity — invalidates all three families. `columnOrder` is the only
|
|
488
|
+
// dependency the header groups declare, and `columnVisibility` the only one
|
|
489
|
+
// the cells do, so both are needed. Remove this once the deps are fixed
|
|
490
|
+
// upstream; the test that fails without it groups two columns and asserts the
|
|
491
|
+
// second one leaves the grid.
|
|
492
|
+
//
|
|
493
|
+
// Writing back into the store from its own subscriber is safe: the guard is
|
|
494
|
+
// on `grouping`'s identity, and neither write touches it, so the callback
|
|
495
|
+
// these writes trigger short-circuits.
|
|
496
|
+
useEffect(() => {
|
|
497
|
+
if (!groupColumnEnabled) return;
|
|
498
|
+
let previousGrouping = table.store.state.grouping;
|
|
499
|
+
|
|
500
|
+
const subscription = table.store.subscribe((state) => {
|
|
501
|
+
if (state.grouping === previousGrouping) return;
|
|
502
|
+
previousGrouping = state.grouping;
|
|
503
|
+
|
|
504
|
+
table.setColumnVisibility((old) => ({
|
|
505
|
+
...old,
|
|
506
|
+
[GROUP_COLUMN_ID]: state.grouping.length > 0,
|
|
507
|
+
}));
|
|
508
|
+
table.setColumnOrder((old) => [...old]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
return () => subscription.unsubscribe();
|
|
512
|
+
}, [groupColumnEnabled, table]);
|
|
513
|
+
|
|
311
514
|
// Mirror every state change back to storage. Subscribing (rather than writing
|
|
312
515
|
// from an effect on a state snapshot) means nothing is missed, including
|
|
313
516
|
// changes made straight through the table API by the consumer.
|
|
517
|
+
//
|
|
518
|
+
// Writes are debounced because `columnResizeMode: "onChange"` publishes a new
|
|
519
|
+
// state on every pointer move of a resize drag, and `setItem` serialises
|
|
520
|
+
// synchronously on the main thread. The trailing edge is enough: storage only
|
|
521
|
+
// has to agree with the table once the user stops.
|
|
314
522
|
useEffect(() => {
|
|
315
523
|
if (!hasPersistenceKeys(persist)) return;
|
|
316
524
|
writePersistedState(table.store.state, persist);
|
|
525
|
+
|
|
526
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
527
|
+
let pending: TableState<TMDataGridFeatures> | null = null;
|
|
528
|
+
|
|
529
|
+
const flush = () => {
|
|
530
|
+
timeout = undefined;
|
|
531
|
+
if (pending === null) return;
|
|
532
|
+
writePersistedState(pending, persist);
|
|
533
|
+
pending = null;
|
|
534
|
+
};
|
|
535
|
+
|
|
317
536
|
const subscription = table.store.subscribe((state) => {
|
|
318
|
-
|
|
537
|
+
pending = state;
|
|
538
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
539
|
+
timeout = setTimeout(flush, PERSIST_DEBOUNCE_MS);
|
|
319
540
|
});
|
|
320
|
-
|
|
541
|
+
|
|
542
|
+
return () => {
|
|
543
|
+
subscription.unsubscribe();
|
|
544
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
545
|
+
// An unmount mid-debounce would otherwise drop the last change.
|
|
546
|
+
flush();
|
|
547
|
+
};
|
|
321
548
|
}, [persist, table]);
|
|
322
549
|
|
|
323
550
|
const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
|
|
@@ -326,6 +553,10 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
326
553
|
columnsPanelOpen: false,
|
|
327
554
|
filterPanelColumnId: null,
|
|
328
555
|
draggedColumnId: null,
|
|
556
|
+
// `useCreateStore` builds the store once per mount, so this is a genuine
|
|
557
|
+
// default rather than a value that would fight later clicks.
|
|
558
|
+
highlightedRowId: defaultHighlightedRowId ?? null,
|
|
559
|
+
selectionAnchorRowId: null,
|
|
329
560
|
},
|
|
330
561
|
({ setState }) => ({
|
|
331
562
|
openFilterPanel: (columnId = null) =>
|
|
@@ -348,13 +579,34 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
348
579
|
setState((prev) => ({ ...prev, draggedColumnId: columnId })),
|
|
349
580
|
endColumnDrag: () =>
|
|
350
581
|
setState((prev) => ({ ...prev, draggedColumnId: null })),
|
|
582
|
+
setHighlightedRow: (rowId) =>
|
|
583
|
+
setState((prev) => ({ ...prev, highlightedRowId: rowId })),
|
|
584
|
+
setSelectionAnchor: (rowId) =>
|
|
585
|
+
setState((prev) => ({ ...prev, selectionAnchorRowId: rowId })),
|
|
351
586
|
}),
|
|
352
587
|
);
|
|
353
588
|
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
|
|
589
|
+
// `onHighlightedRowChange` is fired from a subscription rather than from the store
|
|
590
|
+
// action, so it covers every route to a new active row — a row click, and a
|
|
591
|
+
// consumer calling `setHighlightedRow` itself. Held in a ref because the store is
|
|
592
|
+
// built once and its actions would otherwise close over the first render's
|
|
593
|
+
// callback.
|
|
594
|
+
const onHighlightedRowChangeRef = useRef(onHighlightedRowChange);
|
|
595
|
+
useEffect(() => {
|
|
596
|
+
onHighlightedRowChangeRef.current = onHighlightedRowChange;
|
|
597
|
+
}, [onHighlightedRowChange]);
|
|
598
|
+
|
|
599
|
+
useEffect(() => {
|
|
600
|
+
// Seeded from the current value so mounting with a `defaultHighlightedRowId`
|
|
601
|
+
// does not report a change that nothing made.
|
|
602
|
+
let previous = ui.state.highlightedRowId;
|
|
603
|
+
const subscription = ui.subscribe((state) => {
|
|
604
|
+
if (state.highlightedRowId === previous) return;
|
|
605
|
+
previous = state.highlightedRowId;
|
|
606
|
+
onHighlightedRowChangeRef.current?.(state.highlightedRowId);
|
|
607
|
+
});
|
|
608
|
+
return () => subscription.unsubscribe();
|
|
609
|
+
}, [ui]);
|
|
358
610
|
|
|
359
611
|
return { table, ui, features };
|
|
360
612
|
}
|