@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
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 +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useCreateStore } from "@tanstack/react-store";
|
|
1
|
+
import { useCreateStore, useSelector } from "@tanstack/react-store";
|
|
2
|
+
import { shallow } from "@tanstack/store";
|
|
2
3
|
import {
|
|
3
4
|
aggregationFns,
|
|
4
5
|
type ColumnDef,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
metaHelper,
|
|
24
25
|
type Row,
|
|
25
26
|
type RowData,
|
|
27
|
+
rowAggregationFeature,
|
|
26
28
|
rowExpandingFeature,
|
|
27
29
|
rowPaginationFeature,
|
|
28
30
|
rowPinningFeature,
|
|
@@ -51,13 +53,15 @@ import {
|
|
|
51
53
|
createEditEngine,
|
|
52
54
|
type TMDataGridEditApi,
|
|
53
55
|
type TMDataGridEditCommitArgs,
|
|
54
|
-
type
|
|
56
|
+
type TMDataGridSaveDraftsArgs,
|
|
57
|
+
type TMDataGridSaveDraftsResult,
|
|
55
58
|
type TMDataGridEditEngineContext,
|
|
56
59
|
type TMDataGridColumnEditOptions,
|
|
57
60
|
type TMDataGridEditMode,
|
|
58
61
|
type TMDataGridRowAddArgs,
|
|
59
62
|
type TMDataGridRowDeleteArgs,
|
|
60
63
|
type TMDataGridRowValidators,
|
|
64
|
+
type TMDataGridTableValidators,
|
|
61
65
|
} from "./core/editEngine";
|
|
62
66
|
import {
|
|
63
67
|
emptyValueForOperator,
|
|
@@ -66,6 +70,11 @@ import {
|
|
|
66
70
|
} from "./core/filterOperators";
|
|
67
71
|
import { getColumnDefaultOperator, isControlColumn } from "./core/columnUtils";
|
|
68
72
|
import type { TMDataGridColumnFilterOptions } from "./core/filterControls";
|
|
73
|
+
import {
|
|
74
|
+
resolveFilterOptions,
|
|
75
|
+
type TMDataGridFiltersOptions,
|
|
76
|
+
type TMDataGridFiltersSettings,
|
|
77
|
+
} from "./core/filterSurface";
|
|
69
78
|
import {
|
|
70
79
|
createFuzzyRankedSortedRowModel,
|
|
71
80
|
fuzzyGlobalFilterFn,
|
|
@@ -93,7 +102,29 @@ import {
|
|
|
93
102
|
isSameCell,
|
|
94
103
|
type TMDataGridCellPosition,
|
|
95
104
|
} from "./core/cellNavigation";
|
|
105
|
+
import {
|
|
106
|
+
findFrozenStateSlices,
|
|
107
|
+
stabilizeControlledState,
|
|
108
|
+
withoutUndefinedSlices,
|
|
109
|
+
} from "./core/controlledState";
|
|
110
|
+
import {
|
|
111
|
+
beginControlledStateSync,
|
|
112
|
+
deferControlledStateSyncPublishes,
|
|
113
|
+
endControlledStateSync,
|
|
114
|
+
} from "./core/controlledStateSync";
|
|
115
|
+
import { registerDeletedRows } from "./core/deletedRows";
|
|
116
|
+
import {
|
|
117
|
+
withPageReset,
|
|
118
|
+
type TMDataGridQueryTable,
|
|
119
|
+
} from "./core/pageReset";
|
|
96
120
|
import type { TMDataGridCellRange } from "./core/cellRange";
|
|
121
|
+
import {
|
|
122
|
+
resolveExportOptions,
|
|
123
|
+
type TMDataGridExportOptions,
|
|
124
|
+
type TMDataGridExportPickerRequest,
|
|
125
|
+
type TMDataGridExportSettings,
|
|
126
|
+
type TMDataGridExportValueGetter,
|
|
127
|
+
} from "./core/export";
|
|
97
128
|
import {
|
|
98
129
|
createSelectColumn,
|
|
99
130
|
SELECT_COLUMN_ID,
|
|
@@ -164,8 +195,8 @@ export type TMDataGridColumnMeta = {
|
|
|
164
195
|
*/
|
|
165
196
|
enableOrdering?: boolean;
|
|
166
197
|
/**
|
|
167
|
-
* How this column filters: the operator a fresh
|
|
168
|
-
* value control the filter panel renders for it.
|
|
198
|
+
* How this column filters: which operators it offers, the operator a fresh
|
|
199
|
+
* filter starts with, and the value control the filter panel renders for it.
|
|
169
200
|
*
|
|
170
201
|
* ```tsx
|
|
171
202
|
* meta: {
|
|
@@ -192,6 +223,25 @@ export type TMDataGridColumnMeta = {
|
|
|
192
223
|
* See {@link TMDataGridColumnEditOptions}.
|
|
193
224
|
*/
|
|
194
225
|
edit?: TMDataGridColumnEditOptions;
|
|
226
|
+
/**
|
|
227
|
+
* `false` leaves the column out of every export and out of Ctrl+C - for a
|
|
228
|
+
* column of buttons, or one whose value means nothing outside the grid.
|
|
229
|
+
* Defaults to `true`.
|
|
230
|
+
*/
|
|
231
|
+
enableExport?: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* The value an export writes for this column, in place of
|
|
234
|
+
* `row.getValue(column.id)`. The export otherwise writes the value, never
|
|
235
|
+
* what the cell renders, so this is where a status code becomes its label
|
|
236
|
+
* or a nested object becomes one field.
|
|
237
|
+
*
|
|
238
|
+
* ```tsx
|
|
239
|
+
* meta: {
|
|
240
|
+
* exportValue: ({ value }) => STATUS_LABELS[value as Status],
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
exportValue?: TMDataGridExportValueGetter;
|
|
195
245
|
};
|
|
196
246
|
|
|
197
247
|
/** Grid-wide configuration passed through `options.meta`. */
|
|
@@ -224,6 +274,7 @@ export const tmDataGridFeatures = tableFeatures({
|
|
|
224
274
|
columnResizingFeature,
|
|
225
275
|
columnFacetingFeature,
|
|
226
276
|
columnGroupingFeature,
|
|
277
|
+
rowAggregationFeature,
|
|
227
278
|
// Registered for grouping's sake rather than for tree data: the grouped row
|
|
228
279
|
// model builds the parent rows, and this is what flattens the expanded ones
|
|
229
280
|
// back into the flat list the body virtualizes.
|
|
@@ -287,6 +338,9 @@ const DEFAULT_DETAILS_EST_HEIGHT = 160;
|
|
|
287
338
|
/** Rows kept mounted on each side of the viewport. See `overscan`. */
|
|
288
339
|
const DEFAULT_OVERSCAN = 6;
|
|
289
340
|
|
|
341
|
+
/** One empty array, so a selector answering "none" never changes identity. */
|
|
342
|
+
const EMPTY_IDS: ReadonlyArray<string> = [];
|
|
343
|
+
|
|
290
344
|
/**
|
|
291
345
|
* Chrome state that is *not* table state: which panels are open, and which
|
|
292
346
|
* column opened the filter panel. Kept in a TanStack Store so consumers can
|
|
@@ -294,9 +348,20 @@ const DEFAULT_OVERSCAN = 6;
|
|
|
294
348
|
*/
|
|
295
349
|
export type TMDataGridUiState = {
|
|
296
350
|
filterPanelOpen: boolean;
|
|
297
|
-
|
|
298
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Column whose *panel* row should take the focus. Cleared once the row has
|
|
353
|
+
* taken it, so pointing at the same column twice focuses twice.
|
|
354
|
+
*/
|
|
299
355
|
filterPanelColumnId: string | null;
|
|
356
|
+
/**
|
|
357
|
+
* Column whose *header filter* control should take the focus, under
|
|
358
|
+
* `filters.inHeader`. Cleared once taken, like the one above.
|
|
359
|
+
*
|
|
360
|
+
* Its own slot rather than a second reader of `filterPanelColumnId`: a grid
|
|
361
|
+
* can have header filters and a panel at once, and two controls racing to
|
|
362
|
+
* answer one id means whichever mounted last wins the caret.
|
|
363
|
+
*/
|
|
364
|
+
headerFilterColumnId: string | null;
|
|
300
365
|
/**
|
|
301
366
|
* Column being dragged by its header, if any. Held here rather than read from
|
|
302
367
|
* `dataTransfer`, which browsers keep unreadable until the drop.
|
|
@@ -335,13 +400,33 @@ export type TMDataGridUiState = {
|
|
|
335
400
|
* describe different places.
|
|
336
401
|
*/
|
|
337
402
|
cellRange: TMDataGridCellRange | null;
|
|
403
|
+
/**
|
|
404
|
+
* The export column picker, while it is open: which rows it exports and the
|
|
405
|
+
* options of the item that opened it. `null` while closed. Held here rather
|
|
406
|
+
* than in the menu item, which unmounts with the dropdown the moment it is
|
|
407
|
+
* clicked.
|
|
408
|
+
*/
|
|
409
|
+
exportPicker: TMDataGridExportPickerRequest | null;
|
|
338
410
|
};
|
|
339
411
|
|
|
340
412
|
export type TMDataGridUiActions = {
|
|
341
413
|
openFilterPanel: (columnId?: string | null) => void;
|
|
342
414
|
closeFilterPanel: () => void;
|
|
343
|
-
|
|
344
|
-
|
|
415
|
+
/** Opens the export column picker for `request`. See `TMDataGrid.Menu.Export`'s `columns="custom"`. */
|
|
416
|
+
openExportPicker: (request: TMDataGridExportPickerRequest) => void;
|
|
417
|
+
closeExportPicker: () => void;
|
|
418
|
+
/**
|
|
419
|
+
* Points at a column's row in the filter panel without opening anything.
|
|
420
|
+
* `openFilterPanel` does this as well as opening; this is the half a panel
|
|
421
|
+
* that is already showing needs.
|
|
422
|
+
*/
|
|
423
|
+
focusPanelFilter: (columnId: string | null) => void;
|
|
424
|
+
/**
|
|
425
|
+
* Points at a column's header filter control - what `openColumnFilter` does
|
|
426
|
+
* under `filters.inHeader`, where there is no panel to open. The header row
|
|
427
|
+
* scrolls the column into view and focuses it.
|
|
428
|
+
*/
|
|
429
|
+
focusHeaderFilter: (columnId: string | null) => void;
|
|
345
430
|
startColumnDrag: (columnId: string) => void;
|
|
346
431
|
endColumnDrag: () => void;
|
|
347
432
|
/**
|
|
@@ -387,12 +472,27 @@ export type TMDataGridApi<TData extends RowData> = {
|
|
|
387
472
|
/**
|
|
388
473
|
* The edit engine - open forms, dirty/error projections, and the verbs
|
|
389
474
|
* (`begin`, `commit`, `cancel`, `submitAll`). `edit.getForm(rowId)` hands
|
|
390
|
-
* out the same TanStack Form the inline editors write through
|
|
391
|
-
* or detail panel can share a row's draft
|
|
475
|
+
* out the same TanStack Form the inline editors write through while a row
|
|
476
|
+
* is open, so a drawer or detail panel can share a row's draft; a
|
|
477
|
+
* committed row has no form until `begin` reopens it. Inert until
|
|
478
|
+
* `editing` is set.
|
|
392
479
|
*/
|
|
393
|
-
edit: TMDataGridEditApi
|
|
480
|
+
edit: TMDataGridEditApi<TData>;
|
|
394
481
|
/** Table-level feature switches, re-read from options on every render. */
|
|
395
482
|
features: TMDataGridFeatureFlags;
|
|
483
|
+
/**
|
|
484
|
+
* Where the filter controls live, the `filters` option with its defaults
|
|
485
|
+
* filled in. On the api rather than in a component's props because the
|
|
486
|
+
* pills, the column menu and `openColumnFilter` all have to agree with the
|
|
487
|
+
* table about which surface is on.
|
|
488
|
+
*/
|
|
489
|
+
filters: TMDataGridFiltersSettings;
|
|
490
|
+
/**
|
|
491
|
+
* How the grid exports, the `exportOptions` option with its defaults filled
|
|
492
|
+
* in. Read by `useTMDataGridExport`, the `TMDataGrid.Menu.Export*` items and
|
|
493
|
+
* the cell-range menu, so every export of the grid agrees on the format.
|
|
494
|
+
*/
|
|
495
|
+
exportOptions: TMDataGridExportSettings;
|
|
396
496
|
/** Every string the chrome renders, `labels` merged over the English defaults. */
|
|
397
497
|
labels: TMDataGridLabels;
|
|
398
498
|
/** The detail renderer, when row details are on. See `renderDetails`. */
|
|
@@ -455,6 +555,27 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
|
|
|
455
555
|
* Pathed issues land on the matching columns; pathless ones on the row.
|
|
456
556
|
*/
|
|
457
557
|
rowValidators?: TMDataGridRowValidators;
|
|
558
|
+
/**
|
|
559
|
+
* Rules that need the other rows - no duplicate keys, no overlapping
|
|
560
|
+
* ranges, allocations summing to a total. Handed the committing row and
|
|
561
|
+
* `rows`, the collection as it would stand if the commit landed: every
|
|
562
|
+
* draft overlaid, entry rows appended, deletion-marked rows removed.
|
|
563
|
+
*
|
|
564
|
+
* ```tsx
|
|
565
|
+
* tableValidators: {
|
|
566
|
+
* onSubmit: ({ value, rowId, rows }) =>
|
|
567
|
+
* rows.some((r) => r.rowId !== rowId && r.value.code === value.code)
|
|
568
|
+
* ? { fields: { code: "Duplicate code" } }
|
|
569
|
+
* : undefined,
|
|
570
|
+
* }
|
|
571
|
+
* ```
|
|
572
|
+
*
|
|
573
|
+
* Runs at every commit, after the row's own validators, and again per
|
|
574
|
+
* parked row during `saveDrafts` - a draft another edit has invalidated
|
|
575
|
+
* blocks the save. Pathed issues land on the committing row's cells,
|
|
576
|
+
* pathless ones on the row.
|
|
577
|
+
*/
|
|
578
|
+
tableValidators?: TMDataGridTableValidators<TData>;
|
|
458
579
|
/** Rows the pencil skips - `false` keeps a row read-only in every mode. */
|
|
459
580
|
isRowEditable?: (row: Row<TMDataGridFeatures, TData>) => boolean;
|
|
460
581
|
/**
|
|
@@ -471,39 +592,48 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
|
|
|
471
592
|
/**
|
|
472
593
|
* Seed values for `edit.addRow()` - the entry row's starting point. A
|
|
473
594
|
* function is called per added row (fresh timestamps, empty arrays).
|
|
595
|
+
* `edit.addRow(values)` overrides this key by key for that one row.
|
|
474
596
|
*/
|
|
475
597
|
newRowDefaults?: TData | (() => TData);
|
|
476
598
|
/**
|
|
477
|
-
* Called when an entry row commits: `Enter` or the lane's
|
|
478
|
-
*
|
|
479
|
-
*
|
|
599
|
+
* Called when an entry row commits: `Enter` or the lane's ✓, or
|
|
600
|
+
* `saveDrafts` under `editing.draft`. Create the record and let it arrive
|
|
601
|
+
* back through `data`; the engine's `tempId` never leaves the grid.
|
|
480
602
|
*/
|
|
481
603
|
onRowAdd?: (args: TMDataGridRowAddArgs<TData>) => void | Promise<void>;
|
|
482
604
|
/**
|
|
483
|
-
* Called by `edit.deleteRow`
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
605
|
+
* Called by `edit.deleteRow` - confirmation, if any, belongs in here. Under
|
|
606
|
+
* `editing.draft` deletions accumulate in `edit.state.deletedRowIds`
|
|
607
|
+
* instead and are reported by `saveDrafts`. Setting this also puts the
|
|
608
|
+
* trash can in the edit lane.
|
|
487
609
|
*/
|
|
488
610
|
onRowDelete?: (args: TMDataGridRowDeleteArgs<TData>) => void | Promise<void>;
|
|
489
611
|
};
|
|
490
612
|
|
|
491
613
|
/**
|
|
492
614
|
* The `editing` option: one object that turns editing on and holds
|
|
493
|
-
* everything about it.
|
|
494
|
-
*
|
|
615
|
+
* everything about it. Two axes, and they are independent: `mode` picks what
|
|
616
|
+
* counts as a commit, `draft` picks where that commit goes.
|
|
617
|
+
*
|
|
618
|
+
* | Mode | Commit | Cancel | Controls |
|
|
619
|
+
* | ---- | ------ | ------ | -------- |
|
|
620
|
+
* | `"cell"` | Enter, Tab, blur - Sheets | Escape | none |
|
|
621
|
+
* | `"cellConfirm"` | ✓ or Enter; Tab and blur keep the draft | ✕ or Escape | ✓ / ✕ beside the input |
|
|
622
|
+
* | `"row"` | Save in the edit lane, or Enter | Cancel, or Escape | generated edit lane |
|
|
623
|
+
*
|
|
624
|
+
* | `draft` | Where a commit goes |
|
|
625
|
+
* | ------- | ------------------- |
|
|
626
|
+
* | `false` (default) | Out as it happens - `onCommit`, `onRowAdd`, `onRowDelete` |
|
|
627
|
+
* | `true` | Into the grid's draft store; `edit.saveDrafts()` sends the lot |
|
|
495
628
|
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
* | `"cellConfirm"` | ✓ or Enter only; blur keeps the draft | ✕ or Escape |
|
|
500
|
-
* | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape |
|
|
501
|
-
* | `"draft"` | `edit.submitAll()` | `edit.cancelAll()` |
|
|
629
|
+
* So `{ mode: "row", draft: true }` is "edit a row, the lane's ✓ parks it,
|
|
630
|
+
* the toolbar's Save sends every parked row at once", and
|
|
631
|
+
* `{ mode: "cell", draft: true }` is the same store filled cell by cell.
|
|
502
632
|
*
|
|
503
633
|
* Setting `editing` makes `getRowId` required - drafts are keyed by row id,
|
|
504
634
|
* and the index fallback would name a different record after any sort - and
|
|
505
|
-
* `
|
|
506
|
-
*
|
|
635
|
+
* `onSaveDrafts` exists only under `draft: true`, the one configuration with
|
|
636
|
+
* a draft store to save.
|
|
507
637
|
*
|
|
508
638
|
* The object may be written inline: the callbacks are read through a ref
|
|
509
639
|
* every render, so its identity does not matter.
|
|
@@ -515,33 +645,79 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
|
|
|
515
645
|
* override the rest.
|
|
516
646
|
*/
|
|
517
647
|
export type TMDataGridEditingOptions<TData extends RowData> =
|
|
518
|
-
TMDataGridEditingCallbacks<TData> &
|
|
519
|
-
|
|
648
|
+
TMDataGridEditingCallbacks<TData> & {
|
|
649
|
+
/** What counts as a commit. See the table above. */
|
|
650
|
+
mode: TMDataGridEditMode;
|
|
651
|
+
/**
|
|
652
|
+
* The column ids that take edits, by id. Unset - the default - every
|
|
653
|
+
* column mapping to a data path is editable, which is what a grid whose
|
|
654
|
+
* columns are mostly the record itself wants.
|
|
655
|
+
*
|
|
656
|
+
* Set it for the other shape: a grid of reference data with one or two
|
|
657
|
+
* columns the user maintains, where naming those is shorter and harder to
|
|
658
|
+
* get wrong than switching every other column off one by one.
|
|
659
|
+
*
|
|
660
|
+
* This gates before `meta.edit`, never past it: a column left out takes no
|
|
661
|
+
* edits whatever its own meta says, and a column listed here still answers
|
|
662
|
+
* to its `meta.edit.enabled`.
|
|
663
|
+
*/
|
|
664
|
+
columns?: ReadonlyArray<string>;
|
|
665
|
+
} & (
|
|
520
666
|
| {
|
|
521
|
-
mode: "draft";
|
|
522
667
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
668
|
+
* Commits park in the grid's draft store instead of reaching the
|
|
669
|
+
* consumer, and leave together through `edit.saveDrafts()`. The
|
|
670
|
+
* edit lane gains the change markers and the per-row revert, the
|
|
671
|
+
* trash marks a row for deletion rather than deleting it, and
|
|
672
|
+
* `TMDataGrid.DraftActions` gets something to save.
|
|
673
|
+
*/
|
|
674
|
+
draft: true;
|
|
675
|
+
/**
|
|
676
|
+
* The bulk save: called once by `edit.saveDrafts()` with the
|
|
677
|
+
* whole draft store - committed edits, added rows and deletion
|
|
678
|
+
* marks - so a server can apply it as one transaction. Without it,
|
|
679
|
+
* `saveDrafts` falls back to the per-row
|
|
680
|
+
* {@link TMDataGridEditingCallbacks.onCommit} loop.
|
|
681
|
+
*
|
|
682
|
+
* Rows still open are not in the payload and stay open. Returning
|
|
683
|
+
* nothing saves the whole store and throwing saves none of it;
|
|
684
|
+
* return a {@link TMDataGridSaveDraftsResult} to save part of it.
|
|
685
|
+
*/
|
|
686
|
+
onSaveDrafts?: (
|
|
687
|
+
args: TMDataGridSaveDraftsArgs<TData>,
|
|
688
|
+
) =>
|
|
689
|
+
| void
|
|
690
|
+
| TMDataGridSaveDraftsResult
|
|
691
|
+
| Promise<void | TMDataGridSaveDraftsResult>;
|
|
692
|
+
/**
|
|
693
|
+
* @deprecated Renamed to {@link onSaveDrafts} - it fires when the
|
|
694
|
+
* draft store is saved, not when a row commits into it. Still
|
|
695
|
+
* honoured; removed in a later beta.
|
|
527
696
|
*/
|
|
528
697
|
onCommitDrafts?: (
|
|
529
|
-
args:
|
|
530
|
-
) =>
|
|
698
|
+
args: TMDataGridSaveDraftsArgs<TData>,
|
|
699
|
+
) =>
|
|
700
|
+
| void
|
|
701
|
+
| TMDataGridSaveDraftsResult
|
|
702
|
+
| Promise<void | TMDataGridSaveDraftsResult>;
|
|
531
703
|
/**
|
|
532
|
-
* Keep
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* into is always sticky, but
|
|
536
|
-
* cannot fill the viewport
|
|
704
|
+
* Keep committed entry rows pinned in the sticky entry block until
|
|
705
|
+
* the draft store is saved, out of the body's sort. Off by default:
|
|
706
|
+
* a committed row joins the body rows instead, sorted and filtered
|
|
707
|
+
* with them - the block a row is *typed* into is always sticky, but
|
|
708
|
+
* committed rows scroll, so entering many cannot fill the viewport
|
|
709
|
+
* with sticky chrome.
|
|
537
710
|
*/
|
|
538
711
|
newRowsSticky?: boolean;
|
|
539
712
|
}
|
|
540
713
|
| {
|
|
541
|
-
|
|
542
|
-
|
|
714
|
+
/** Every commit reaches the consumer as it happens. The default. */
|
|
715
|
+
draft?: false;
|
|
716
|
+
/** Only `draft: true` has a store to save - see the other branch. */
|
|
717
|
+
onSaveDrafts?: never;
|
|
718
|
+
/** @deprecated See {@link onSaveDrafts}. */
|
|
543
719
|
onCommitDrafts?: never;
|
|
544
|
-
/**
|
|
720
|
+
/** Parked entry rows exist only under `draft: true` - see there. */
|
|
545
721
|
newRowsSticky?: never;
|
|
546
722
|
}
|
|
547
723
|
);
|
|
@@ -606,12 +782,55 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
606
782
|
* no extra flag.
|
|
607
783
|
*/
|
|
608
784
|
enablePagination?: boolean;
|
|
785
|
+
/**
|
|
786
|
+
* Sends the grid back to the first page whenever the query changes - a
|
|
787
|
+
* column filter, the quick search, the sort or the grouping. On by
|
|
788
|
+
* default. TanStack's own `autoResetPageIndex` is switched off by the
|
|
789
|
+
* grid: it fires on any change to the `data` array, which under
|
|
790
|
+
* `editing.draft` is every commit.
|
|
791
|
+
*
|
|
792
|
+
* Server-side, `pageIndex` is a position in a result set the grid does not
|
|
793
|
+
* own: narrowing the query leaves it pointing past the last page, and the
|
|
794
|
+
* next request comes back empty. The reset is applied in the same event as
|
|
795
|
+
* the change, so one request goes out, for the first page of the new query.
|
|
796
|
+
*/
|
|
797
|
+
resetPageOnQueryChange?: boolean;
|
|
609
798
|
/**
|
|
610
799
|
* The row-number gutter: a generated lane, outermost left, numbering the
|
|
611
800
|
* rows of the current view - sorted, filtered, continuing across pages,
|
|
612
801
|
* with group rows unnumbered. Off by default.
|
|
613
802
|
*/
|
|
614
803
|
enableRowNumbers?: boolean;
|
|
804
|
+
/**
|
|
805
|
+
* Where the grid puts its filter controls - a popup over the rows, a sidebar
|
|
806
|
+
* beside them, controls in the header row, or nowhere at all so you place
|
|
807
|
+
* `TMDataGrid.FilterPanel` yourself.
|
|
808
|
+
*
|
|
809
|
+
* ```tsx
|
|
810
|
+
* useTMDataGrid({ data, columns, filters: { surface: "sidebar", inHeader: true } });
|
|
811
|
+
* ```
|
|
812
|
+
*
|
|
813
|
+
* Defaults to `{ surface: "popup" }` - the floating panel the grid has
|
|
814
|
+
* always shown. See {@link TMDataGridFiltersOptions}.
|
|
815
|
+
*
|
|
816
|
+
* Read field by field, so a literal is fine here - unlike `labels` or
|
|
817
|
+
* `persist`, this one does not have to be referentially stable.
|
|
818
|
+
*/
|
|
819
|
+
filters?: TMDataGridFiltersOptions;
|
|
820
|
+
/**
|
|
821
|
+
* How the grid exports: the file format, the file name and whether the
|
|
822
|
+
* column labels go in as the first row. Defaults to `csvExcelFormat()`,
|
|
823
|
+
* `"export"` and `true`. See {@link TMDataGridExportOptions}.
|
|
824
|
+
*
|
|
825
|
+
* ```tsx
|
|
826
|
+
* useTMDataGrid({ data, columns, exportOptions: { format: csvFormat(), fileName: "employees" } });
|
|
827
|
+
* ```
|
|
828
|
+
*
|
|
829
|
+
* Read field by field like `filters`, so a literal is fine. A `format`
|
|
830
|
+
* built inline is rebuilt every render, which costs nothing but a small
|
|
831
|
+
* object; keep it at module scope when that bothers you.
|
|
832
|
+
*/
|
|
833
|
+
exportOptions?: TMDataGridExportOptions;
|
|
615
834
|
/**
|
|
616
835
|
* How the quick search (`TMDataGrid.Search`) matches. `"fuzzy"` - the
|
|
617
836
|
* default - forgives typos and skipped characters, and while it is the
|
|
@@ -875,6 +1094,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
875
1094
|
labels: labelsOverride,
|
|
876
1095
|
enableColumnOrdering,
|
|
877
1096
|
enablePagination,
|
|
1097
|
+
resetPageOnQueryChange,
|
|
878
1098
|
enableRowNumbers,
|
|
879
1099
|
selectionMode,
|
|
880
1100
|
showSelectedBackground,
|
|
@@ -882,6 +1102,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
882
1102
|
onHighlightedRowChange,
|
|
883
1103
|
cellSelection,
|
|
884
1104
|
onFocusedCellChange,
|
|
1105
|
+
filters: filterOptions,
|
|
1106
|
+
exportOptions: exportOptionsOverride,
|
|
885
1107
|
editing,
|
|
886
1108
|
renderDetails,
|
|
887
1109
|
renderDetailsEstHeight = DEFAULT_DETAILS_EST_HEIGHT,
|
|
@@ -891,6 +1113,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
891
1113
|
// The one place `editing` is unpacked - the engine and the flags keep their
|
|
892
1114
|
// own vocabulary (`editMode`, `onEditCommit`), so the mapping lives here.
|
|
893
1115
|
const editMode = editing?.mode;
|
|
1116
|
+
const editDraft = editing?.draft === true;
|
|
894
1117
|
|
|
895
1118
|
// Derived up here, rather than just before the return, because the rest of the
|
|
896
1119
|
// hook needs `selectColumn` - one place decides what each mode means.
|
|
@@ -912,6 +1135,50 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
912
1135
|
// Resolved on the override's identity, so a module-scope dictionary costs one
|
|
913
1136
|
// merge for the lifetime of the grid.
|
|
914
1137
|
const labels = useMemo(() => mergeLabels(labelsOverride), [labelsOverride]);
|
|
1138
|
+
// Field by field for the same reason as `filters` below.
|
|
1139
|
+
const {
|
|
1140
|
+
format: exportFormat,
|
|
1141
|
+
fileName: exportFileName,
|
|
1142
|
+
includeHeaders: exportIncludeHeaders,
|
|
1143
|
+
columns: exportColumns,
|
|
1144
|
+
} = exportOptionsOverride ?? {};
|
|
1145
|
+
const exportOptions = useMemo(
|
|
1146
|
+
() =>
|
|
1147
|
+
resolveExportOptions({
|
|
1148
|
+
format: exportFormat,
|
|
1149
|
+
fileName: exportFileName,
|
|
1150
|
+
includeHeaders: exportIncludeHeaders,
|
|
1151
|
+
columns: exportColumns,
|
|
1152
|
+
}),
|
|
1153
|
+
[exportFormat, exportFileName, exportIncludeHeaders, exportColumns],
|
|
1154
|
+
);
|
|
1155
|
+
// Unpacked before the memo, so the api is keyed on the five fields rather
|
|
1156
|
+
// than on the object's identity - which is what lets `filters` be written as
|
|
1157
|
+
// a literal, the way it reads best, without republishing every render.
|
|
1158
|
+
const {
|
|
1159
|
+
surface: filterSurface,
|
|
1160
|
+
sidebarSide: filterSidebarSide,
|
|
1161
|
+
sidebarWidth: filterSidebarWidth,
|
|
1162
|
+
defaultOpen: filtersDefaultOpen,
|
|
1163
|
+
inHeader: filtersInHeader,
|
|
1164
|
+
} = filterOptions ?? {};
|
|
1165
|
+
const filters = useMemo(
|
|
1166
|
+
() =>
|
|
1167
|
+
resolveFilterOptions({
|
|
1168
|
+
surface: filterSurface,
|
|
1169
|
+
sidebarSide: filterSidebarSide,
|
|
1170
|
+
sidebarWidth: filterSidebarWidth,
|
|
1171
|
+
defaultOpen: filtersDefaultOpen,
|
|
1172
|
+
inHeader: filtersInHeader,
|
|
1173
|
+
}),
|
|
1174
|
+
[
|
|
1175
|
+
filterSurface,
|
|
1176
|
+
filterSidebarSide,
|
|
1177
|
+
filterSidebarWidth,
|
|
1178
|
+
filtersDefaultOpen,
|
|
1179
|
+
filtersInHeader,
|
|
1180
|
+
],
|
|
1181
|
+
);
|
|
915
1182
|
|
|
916
1183
|
const pinningEnabled = options.enableColumnPinning !== false;
|
|
917
1184
|
const selectColumnEnabled = features.selectColumn;
|
|
@@ -921,14 +1188,12 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
921
1188
|
const detailsColumnEnabled = renderDetails !== undefined;
|
|
922
1189
|
// Row mode's Save sits at the end of the row - the lane is its chrome. It
|
|
923
1190
|
// also appears wherever the trash can has somewhere to report to, and
|
|
924
|
-
// always under draft
|
|
925
|
-
// revert - with or without `
|
|
926
|
-
//
|
|
1191
|
+
// always under `draft`, where it is the change marker and the per-row
|
|
1192
|
+
// revert - with or without `onSaveDrafts`, since the per-row fallback is a
|
|
1193
|
+
// first-class configuration.
|
|
927
1194
|
const editColumnEnabled =
|
|
928
1195
|
editing !== undefined &&
|
|
929
|
-
(editing.mode === "row" ||
|
|
930
|
-
editing.mode === "draft" ||
|
|
931
|
-
editing.onRowDelete !== undefined);
|
|
1196
|
+
(editing.mode === "row" || editDraft || editing.onRowDelete !== undefined);
|
|
932
1197
|
|
|
933
1198
|
// The generated lanes bake `meta.label` into their definitions, so the memo
|
|
934
1199
|
// depends on the strings rather than on the labels object - a fresh
|
|
@@ -939,9 +1204,9 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
939
1204
|
const editColumnLabel = labels.editColumnLabel;
|
|
940
1205
|
const rowNumberColumnLabel = labels.rowNumberColumnLabel;
|
|
941
1206
|
const rowNumbersEnabled = features.rowNumbers;
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
const
|
|
1207
|
+
// A draft lane holds three controls (state icon, undo, trash) where the
|
|
1208
|
+
// rest hold two - it gets the wider track.
|
|
1209
|
+
const editWideLane = editDraft;
|
|
945
1210
|
|
|
946
1211
|
const columns = useMemo(() => {
|
|
947
1212
|
const base = withTMDataGridDefaults<TData>(
|
|
@@ -972,7 +1237,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
972
1237
|
...base,
|
|
973
1238
|
// Last and pinned right - the row's Save belongs at the end of the row.
|
|
974
1239
|
...(editColumnEnabled
|
|
975
|
-
? [createEditColumn<TData>(editColumnLabel,
|
|
1240
|
+
? [createEditColumn<TData>(editColumnLabel, editWideLane)]
|
|
976
1241
|
: []),
|
|
977
1242
|
];
|
|
978
1243
|
}, [
|
|
@@ -982,7 +1247,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
982
1247
|
detailsColumnEnabled,
|
|
983
1248
|
groupColumnEnabled,
|
|
984
1249
|
editColumnEnabled,
|
|
985
|
-
|
|
1250
|
+
editWideLane,
|
|
986
1251
|
rowNumberColumnLabel,
|
|
987
1252
|
selectColumnLabel,
|
|
988
1253
|
groupColumnLabel,
|
|
@@ -1004,8 +1269,213 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1004
1269
|
const initialGrouping =
|
|
1005
1270
|
persistedState.grouping ?? options.initialState?.grouping ?? [];
|
|
1006
1271
|
|
|
1272
|
+
// Current grouping state, feeding the tree column's entry in a controlled
|
|
1273
|
+
// `columnVisibility` (below). A ref, not state: it is only read while
|
|
1274
|
+
// building the options, and the store change that updates it re-renders the
|
|
1275
|
+
// hook anyway. A controlled `grouping` takes precedence over the persisted
|
|
1276
|
+
// one; grouping held in an external atom is not readable here and is
|
|
1277
|
+
// corrected by the effect below with one extra write on mount.
|
|
1278
|
+
const groupingActiveRef = useRef(
|
|
1279
|
+
(options.state?.grouping ?? initialGrouping).length > 0,
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
// Removes keys set to `undefined` before TanStack writes them into the
|
|
1283
|
+
// slice atoms; such a key means the slice is not controlled.
|
|
1284
|
+
const consumerState = withoutUndefinedSlices(options.state);
|
|
1285
|
+
|
|
1286
|
+
// A controlled `columnVisibility` replaces the whole map on every options
|
|
1287
|
+
// sync, including the grid's own entries, so the tree column's entry must be
|
|
1288
|
+
// re-applied here. Entries for the control columns are removed, as they are
|
|
1289
|
+
// for `initialState`: their visibility follows the feature options, not the
|
|
1290
|
+
// visibility map.
|
|
1291
|
+
const controlledColumnVisibility = consumerState?.columnVisibility;
|
|
1292
|
+
const requestedState =
|
|
1293
|
+
consumerState !== undefined && controlledColumnVisibility !== undefined
|
|
1294
|
+
? {
|
|
1295
|
+
...consumerState,
|
|
1296
|
+
columnVisibility: {
|
|
1297
|
+
...withoutControlColumnVisibility(controlledColumnVisibility),
|
|
1298
|
+
...(groupColumnEnabled
|
|
1299
|
+
? { [GROUP_COLUMN_ID]: groupingActiveRef.current }
|
|
1300
|
+
: {}),
|
|
1301
|
+
},
|
|
1302
|
+
}
|
|
1303
|
+
: consumerState;
|
|
1304
|
+
|
|
1305
|
+
// Controlled state is synced to the table on every render and compared by
|
|
1306
|
+
// identity, so a `state` object built in the consumer's render body would
|
|
1307
|
+
// cause an infinite render loop. Unchanged slices are forwarded with the
|
|
1308
|
+
// previous render's identity instead - see controlledState.ts.
|
|
1309
|
+
const controlledStateRef = useRef<Partial<TableState<TMDataGridFeatures>>>(
|
|
1310
|
+
undefined,
|
|
1311
|
+
);
|
|
1312
|
+
// Exception: the grouping workaround below republishes `columnOrder` and
|
|
1313
|
+
// `columnVisibility` with unchanged contents and a new identity to repair
|
|
1314
|
+
// table-core's missing memo deps - the exact write stabilization cancels.
|
|
1315
|
+
// After it runs, one render forwards the controlled state unstabilized so
|
|
1316
|
+
// the new identities reach the atoms. A single render cannot loop.
|
|
1317
|
+
const republishControlledStateRef = useRef(false);
|
|
1318
|
+
const controlledState = republishControlledStateRef.current
|
|
1319
|
+
? requestedState
|
|
1320
|
+
: stabilizeControlledState(requestedState, controlledStateRef.current);
|
|
1321
|
+
republishControlledStateRef.current = false;
|
|
1322
|
+
controlledStateRef.current = controlledState;
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* What the hook's own subscription watches.
|
|
1326
|
+
*
|
|
1327
|
+
* `useTable` re-renders whoever called it whenever a state slice changes
|
|
1328
|
+
* identity, and `columnResizing` publishes a new delta on every pointer move
|
|
1329
|
+
* of a resize drag - so without this the consumer's component, and the whole
|
|
1330
|
+
* grid under it, would re-render for each of those moves. The slice is
|
|
1331
|
+
* pinned to the identity it had when the drag started: a drag starting and a
|
|
1332
|
+
* drag ending still publish, the deltas in between do not.
|
|
1333
|
+
*
|
|
1334
|
+
* The live deltas stay on `table.store.state`, which is where the grid reads
|
|
1335
|
+
* them - see the resize preview in TMDataGridTable.
|
|
1336
|
+
*/
|
|
1337
|
+
const pinnedResizingRef = useRef<
|
|
1338
|
+
TableState<TMDataGridFeatures>["columnResizing"] | null
|
|
1339
|
+
>(null);
|
|
1340
|
+
const selectSettledState = useCallback(
|
|
1341
|
+
(state: TableState<TMDataGridFeatures>) => {
|
|
1342
|
+
const resizing = state.columnResizing;
|
|
1343
|
+
const pinned = pinnedResizingRef.current;
|
|
1344
|
+
if (
|
|
1345
|
+
pinned === null ||
|
|
1346
|
+
pinned.isResizingColumn !== resizing.isResizingColumn
|
|
1347
|
+
) {
|
|
1348
|
+
pinnedResizingRef.current = resizing;
|
|
1349
|
+
return state;
|
|
1350
|
+
}
|
|
1351
|
+
return { ...state, columnResizing: pinned };
|
|
1352
|
+
},
|
|
1353
|
+
[],
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
// The edit engine. Built once per mount; everything it needs later - the
|
|
1357
|
+
// table, the mode, the consumer's callbacks - is read through a ref updated
|
|
1358
|
+
// every render, so forms created at `begin()` always call the latest
|
|
1359
|
+
// `onEditCommit` (the onHighlightedRowChangeRef pattern, applied wholesale).
|
|
1360
|
+
// Created ahead of the table because the table's `data` reads its store;
|
|
1361
|
+
// the ref is filled in once the table exists, and the engine only reads it
|
|
1362
|
+
// inside verbs, never while being built.
|
|
1363
|
+
const editContextRef = useRef<TMDataGridEditEngineContext>(null as never);
|
|
1364
|
+
// The engine is erased; the row type comes back on the way out, which is
|
|
1365
|
+
// what makes `edit.addRow(values)` check against `TData`.
|
|
1366
|
+
const [engine] = useState(() =>
|
|
1367
|
+
createEditEngine(() => editContextRef.current),
|
|
1368
|
+
);
|
|
1369
|
+
const edit = engine as unknown as TMDataGridEditApi<TData>;
|
|
1370
|
+
|
|
1371
|
+
// The rows as shown. Under `editing.draft` a committed row is a row like any
|
|
1372
|
+
// other to the table: its draft replaces the consumer's record and a
|
|
1373
|
+
// committed entry row is prepended, so sorting, filtering, grouping and
|
|
1374
|
+
// aggregates all read the draft store's values. Rows still being typed
|
|
1375
|
+
// into are not here - their values are undecided, and re-sorting under a
|
|
1376
|
+
// caret is not something anyone wants. `newRowsSticky` keeps committed
|
|
1377
|
+
// entry rows in the entry block, so those stay out too.
|
|
1378
|
+
//
|
|
1379
|
+
// Everything is keyed on identities the engine keeps stable while nothing
|
|
1380
|
+
// is decided, so a keystroke in an open editor never rebuilds the model.
|
|
1381
|
+
// `committedValues` rather than `committedRowIds`: the snapshot outlives a
|
|
1382
|
+
// reopen, so a parked row being edited again keeps its place until the
|
|
1383
|
+
// next commit or a cancel decides otherwise.
|
|
1384
|
+
const editCommittedValues = useSelector(
|
|
1385
|
+
edit.store,
|
|
1386
|
+
(state) => state.committedValues,
|
|
1387
|
+
);
|
|
1388
|
+
const newRowsSticky = features.editNewRowsSticky;
|
|
1389
|
+
const editCreatedIds = useSelector(
|
|
1390
|
+
edit.store,
|
|
1391
|
+
(state) =>
|
|
1392
|
+
newRowsSticky
|
|
1393
|
+
? EMPTY_IDS
|
|
1394
|
+
: state.newRows
|
|
1395
|
+
.filter((newRow) => newRow.committed)
|
|
1396
|
+
.map((newRow) => newRow.tempId),
|
|
1397
|
+
{ compare: shallow },
|
|
1398
|
+
);
|
|
1399
|
+
// Read through a ref, not listed as a dependency: an inline `getRowId` is
|
|
1400
|
+
// a fresh function every render, and the merged array must keep its
|
|
1401
|
+
// identity across renders or the table rebuilds its row models on each.
|
|
1402
|
+
const consumerGetRowIdRef = useRef(options.getRowId);
|
|
1403
|
+
consumerGetRowIdRef.current = options.getRowId;
|
|
1404
|
+
const shown = useMemo(() => {
|
|
1405
|
+
const consumerGetRowId = consumerGetRowIdRef.current;
|
|
1406
|
+
const idOf = new Map<object, string>();
|
|
1407
|
+
const created: Array<TData> = [];
|
|
1408
|
+
for (const tempId of editCreatedIds) {
|
|
1409
|
+
const values = editCommittedValues[tempId];
|
|
1410
|
+
if (values === undefined) continue;
|
|
1411
|
+
idOf.set(values, tempId);
|
|
1412
|
+
created.push(values as TData);
|
|
1413
|
+
}
|
|
1414
|
+
const snapshots = Object.keys(editCommittedValues).length;
|
|
1415
|
+
if (snapshots === 0) return { rows: options.data, idOf };
|
|
1416
|
+
// Every snapshot may stand in for a record; only the created ones are
|
|
1417
|
+
// known not to. The rest are looked up by the record's own id.
|
|
1418
|
+
const rows =
|
|
1419
|
+
snapshots === created.length
|
|
1420
|
+
? options.data
|
|
1421
|
+
: options.data.map((record, index) => {
|
|
1422
|
+
const rowId =
|
|
1423
|
+
consumerGetRowId?.(record, index, undefined) ?? String(index);
|
|
1424
|
+
const values = editCommittedValues[rowId];
|
|
1425
|
+
if (values === undefined) return record;
|
|
1426
|
+
idOf.set(values, rowId);
|
|
1427
|
+
return values as TData;
|
|
1428
|
+
});
|
|
1429
|
+
return {
|
|
1430
|
+
rows: created.length === 0 ? rows : [...created, ...rows],
|
|
1431
|
+
idOf,
|
|
1432
|
+
};
|
|
1433
|
+
}, [options.data, editCommittedValues, editCreatedIds]);
|
|
1434
|
+
const shownIdOfRef = useRef(shown.idOf);
|
|
1435
|
+
shownIdOfRef.current = shown.idOf;
|
|
1436
|
+
// A stable id resolver over the merged rows. A draft answers the id of the
|
|
1437
|
+
// record it stands in for - the consumer's `getRowId` never sees a draft
|
|
1438
|
+
// object, so an editable id field cannot rename the row - and a created
|
|
1439
|
+
// row answers its temp id. Everything else is the consumer's own, or
|
|
1440
|
+
// TanStack's index fallback.
|
|
1441
|
+
const shownGetRowId = useCallback(
|
|
1442
|
+
(record: TData, index: number, parent?: Row<TMDataGridFeatures, TData>) =>
|
|
1443
|
+
shownIdOfRef.current.get(record) ??
|
|
1444
|
+
consumerGetRowIdRef.current?.(record, index, parent) ??
|
|
1445
|
+
(parent !== undefined ? `${parent.id}.${index}` : String(index)),
|
|
1446
|
+
[],
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
// Filled immediately after the call below. The query-change wrappers close
|
|
1450
|
+
// over it rather than over `table`, since they are built as part of the
|
|
1451
|
+
// options the table is constructed from.
|
|
1452
|
+
const tableRef = useRef<TMDataGridTable<TData>>(null as never);
|
|
1453
|
+
// A narrower query invalidates the page the grid is on - see pageReset.ts.
|
|
1454
|
+
// On by default everywhere: TanStack's own `autoResetPageIndex` is switched
|
|
1455
|
+
// off below, since it also fires on every draft commit.
|
|
1456
|
+
const resetPage = resetPageOnQueryChange ?? true;
|
|
1457
|
+
const getQueryTable = useCallback(
|
|
1458
|
+
() => tableRef.current as unknown as TMDataGridQueryTable,
|
|
1459
|
+
[],
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
// The sync of `state` into the table's atoms happens inside this call, in
|
|
1463
|
+
// the render body, and publishing from there makes React warn about the
|
|
1464
|
+
// consumer's component - see controlledStateSync.ts.
|
|
1465
|
+
beginControlledStateSync();
|
|
1007
1466
|
const table = useTable({
|
|
1008
|
-
|
|
1467
|
+
// The grid paints a running drag itself, one style write per frame - see
|
|
1468
|
+
// the resize preview in TMDataGridTable - and takes the width into state
|
|
1469
|
+
// once, when the pointer is released. `"onChange"` publishes a width on
|
|
1470
|
+
// every pointer move instead, which re-renders the grid for each of them.
|
|
1471
|
+
columnResizeMode: "onEnd",
|
|
1472
|
+
// TanStack resets both whenever the `data` array's identity changes, and
|
|
1473
|
+
// the rows as shown are a new array on every draft commit (see `shown`),
|
|
1474
|
+
// so a commit on page 3 would land on page 1 with every details panel
|
|
1475
|
+
// closed. Off here; the page reset the grid does want - on a query
|
|
1476
|
+
// change - is its own, below. A consumer's explicit option still wins.
|
|
1477
|
+
autoResetExpanded: false,
|
|
1478
|
+
autoResetPageIndex: false,
|
|
1009
1479
|
enableSorting: true,
|
|
1010
1480
|
enableColumnResizing: true,
|
|
1011
1481
|
// The quick search's matcher. Fuzzy by default (Q4); `"contains"` keeps
|
|
@@ -1022,6 +1492,41 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1022
1492
|
// `"reorder"` to keep the column and have it moved to the front instead.
|
|
1023
1493
|
groupedColumnMode: "remove",
|
|
1024
1494
|
...options,
|
|
1495
|
+
// The query slices, wrapped so a change also takes the grid back to the
|
|
1496
|
+
// first page - see pageReset.ts. Spread conditionally: the keys carry a
|
|
1497
|
+
// `makeStateUpdater` default, and an explicit `undefined` would overwrite
|
|
1498
|
+
// it and leave the slice unwritable.
|
|
1499
|
+
...(resetPage
|
|
1500
|
+
? {
|
|
1501
|
+
onColumnFiltersChange: withPageReset(
|
|
1502
|
+
"columnFilters",
|
|
1503
|
+
options.onColumnFiltersChange as never,
|
|
1504
|
+
getQueryTable,
|
|
1505
|
+
) as TableOptions<
|
|
1506
|
+
TMDataGridFeatures,
|
|
1507
|
+
TData
|
|
1508
|
+
>["onColumnFiltersChange"],
|
|
1509
|
+
onGlobalFilterChange: withPageReset(
|
|
1510
|
+
"globalFilter",
|
|
1511
|
+
options.onGlobalFilterChange as never,
|
|
1512
|
+
getQueryTable,
|
|
1513
|
+
) as TableOptions<TMDataGridFeatures, TData>["onGlobalFilterChange"],
|
|
1514
|
+
onSortingChange: withPageReset(
|
|
1515
|
+
"sorting",
|
|
1516
|
+
options.onSortingChange as never,
|
|
1517
|
+
getQueryTable,
|
|
1518
|
+
) as TableOptions<TMDataGridFeatures, TData>["onSortingChange"],
|
|
1519
|
+
onGroupingChange: withPageReset(
|
|
1520
|
+
"grouping",
|
|
1521
|
+
options.onGroupingChange as never,
|
|
1522
|
+
getQueryTable,
|
|
1523
|
+
) as TableOptions<TMDataGridFeatures, TData>["onGroupingChange"],
|
|
1524
|
+
}
|
|
1525
|
+
: {}),
|
|
1526
|
+
// The rows as shown - see `shown` above. The consumer's own array passes
|
|
1527
|
+
// through untouched while nothing is committed.
|
|
1528
|
+
data: shown.rows,
|
|
1529
|
+
getRowId: shownGetRowId,
|
|
1025
1530
|
// Row details ride on `expanded`, the same state the tree uses - but a data
|
|
1026
1531
|
// row answers `getCanExpand()` false, since TanStack's fallback is
|
|
1027
1532
|
// `subRows.length > 0`. `() => true` is the right answer for a group row
|
|
@@ -1040,8 +1545,24 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1040
1545
|
(typeof options.enableRowPinning === "function"
|
|
1041
1546
|
? options.enableRowPinning(row)
|
|
1042
1547
|
: options.enableRowPinning === true),
|
|
1548
|
+
// A deletion-marked row is not selectable: it is on its way out, and a
|
|
1549
|
+
// bulk action over the selection must not see it. TanStack reads the
|
|
1550
|
+
// predicate on every call, so the mark is checked live against the
|
|
1551
|
+
// engine. Only under `editing.draft`, the one place marks exist; the
|
|
1552
|
+
// consumer's own option keeps the final say, predicate form included.
|
|
1553
|
+
...(editDraft
|
|
1554
|
+
? {
|
|
1555
|
+
enableRowSelection: (row: Row<TMDataGridFeatures, TData>) =>
|
|
1556
|
+
!engine.isRowDeleted(row.id) &&
|
|
1557
|
+
(typeof options.enableRowSelection === "function"
|
|
1558
|
+
? options.enableRowSelection(row)
|
|
1559
|
+
: options.enableRowSelection !== false),
|
|
1560
|
+
}
|
|
1561
|
+
: {}),
|
|
1043
1562
|
features: tmDataGridFeatures,
|
|
1044
1563
|
columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
|
|
1564
|
+
// The stabilized controlled state; `undefined` when nothing is controlled.
|
|
1565
|
+
state: controlledState,
|
|
1045
1566
|
initialState: {
|
|
1046
1567
|
...options.initialState,
|
|
1047
1568
|
...persistedState,
|
|
@@ -1059,14 +1580,14 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1059
1580
|
columnPinning: {
|
|
1060
1581
|
// The generated columns are structurally pinned, so they are re-applied
|
|
1061
1582
|
// on top of anything restored from storage.
|
|
1062
|
-
|
|
1583
|
+
start: [
|
|
1063
1584
|
...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
|
|
1064
1585
|
...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
1065
1586
|
...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
|
|
1066
1587
|
...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
|
|
1067
1588
|
...(
|
|
1068
|
-
persistedState.columnPinning?.
|
|
1069
|
-
options.initialState?.columnPinning?.
|
|
1589
|
+
persistedState.columnPinning?.start ??
|
|
1590
|
+
options.initialState?.columnPinning?.start ??
|
|
1070
1591
|
[]
|
|
1071
1592
|
).filter(
|
|
1072
1593
|
(id) =>
|
|
@@ -1078,10 +1599,10 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1078
1599
|
],
|
|
1079
1600
|
// The edit lane mirrors the generated columns on the left: structurally
|
|
1080
1601
|
// pinned, outermost, re-applied over anything restored.
|
|
1081
|
-
|
|
1602
|
+
end: [
|
|
1082
1603
|
...(
|
|
1083
|
-
persistedState.columnPinning?.
|
|
1084
|
-
options.initialState?.columnPinning?.
|
|
1604
|
+
persistedState.columnPinning?.end ??
|
|
1605
|
+
options.initialState?.columnPinning?.end ??
|
|
1085
1606
|
[]
|
|
1086
1607
|
).filter((id) => id !== EDIT_COLUMN_ID),
|
|
1087
1608
|
...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
|
|
@@ -1094,42 +1615,69 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1094
1615
|
...persistedState.pagination,
|
|
1095
1616
|
},
|
|
1096
1617
|
},
|
|
1097
|
-
});
|
|
1618
|
+
}, selectSettledState);
|
|
1619
|
+
endControlledStateSync();
|
|
1620
|
+
tableRef.current = table as unknown as TMDataGridTable<TData>;
|
|
1621
|
+
deferControlledStateSyncPublishes(table.store);
|
|
1098
1622
|
|
|
1099
|
-
// The
|
|
1100
|
-
// table, the mode, the consumer's callbacks - is read through a ref updated
|
|
1101
|
-
// every render, so forms created at `begin()` always call the latest
|
|
1102
|
-
// `onEditCommit` (the onHighlightedRowChangeRef pattern, applied wholesale).
|
|
1103
|
-
const editContextRef = useRef<TMDataGridEditEngineContext>(null as never);
|
|
1623
|
+
// The engine's view of this render - see the engine's creation above.
|
|
1104
1624
|
editContextRef.current = {
|
|
1105
1625
|
table: table as unknown as TMDataGridTable<TMDataGridRowData>,
|
|
1106
1626
|
editMode: editMode ?? "cell",
|
|
1627
|
+
draft: editDraft,
|
|
1107
1628
|
rowValidators: editing?.rowValidators,
|
|
1629
|
+
tableValidators: editing?.tableValidators as
|
|
1630
|
+
TMDataGridEditEngineContext["tableValidators"],
|
|
1631
|
+
editableColumnIds: editing?.columns,
|
|
1108
1632
|
isRowEditable:
|
|
1109
1633
|
editing?.isRowEditable as TMDataGridEditEngineContext["isRowEditable"],
|
|
1110
1634
|
onEditCommit:
|
|
1111
1635
|
editing?.onCommit as TMDataGridEditEngineContext["onEditCommit"],
|
|
1112
|
-
|
|
1113
|
-
|
|
1636
|
+
// The deprecated name still works; the new one wins if both are set.
|
|
1637
|
+
onSaveDrafts: (editing?.onSaveDrafts ??
|
|
1638
|
+
editing?.onCommitDrafts) as TMDataGridEditEngineContext["onSaveDrafts"],
|
|
1114
1639
|
newRowDefaults:
|
|
1115
1640
|
editing?.newRowDefaults as TMDataGridEditEngineContext["newRowDefaults"],
|
|
1116
1641
|
onRowAdd: editing?.onRowAdd as TMDataGridEditEngineContext["onRowAdd"],
|
|
1117
1642
|
onRowDelete:
|
|
1118
1643
|
editing?.onRowDelete as TMDataGridEditEngineContext["onRowDelete"],
|
|
1119
1644
|
};
|
|
1120
|
-
const [edit] = useState(() =>
|
|
1121
|
-
createEditEngine(() => editContextRef.current),
|
|
1122
|
-
);
|
|
1123
1645
|
|
|
1124
|
-
// Switching
|
|
1125
|
-
// what an open form means, and carrying one across is how a
|
|
1126
|
-
//
|
|
1127
|
-
const
|
|
1646
|
+
// Switching either axis mid-flight drops every draft: the policies disagree
|
|
1647
|
+
// about what an open form means, and carrying one across is how a row
|
|
1648
|
+
// parked for `saveDrafts` silently commits to the consumer instead.
|
|
1649
|
+
const previousEditPolicyRef = useRef({ editMode, editDraft });
|
|
1128
1650
|
useEffect(() => {
|
|
1129
|
-
|
|
1130
|
-
|
|
1651
|
+
const previous = previousEditPolicyRef.current;
|
|
1652
|
+
if (previous.editMode === editMode && previous.editDraft === editDraft) {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
previousEditPolicyRef.current = { editMode, editDraft };
|
|
1131
1656
|
edit.cancelAll();
|
|
1132
|
-
}, [editMode, edit]);
|
|
1657
|
+
}, [editMode, editDraft, edit]);
|
|
1658
|
+
|
|
1659
|
+
// A refetch that drops a record takes the engine's state for it along -
|
|
1660
|
+
// see forgetMissingRows. Not where the grid does not own the result set:
|
|
1661
|
+
// there a row missing from `data` is on another page or filtered away
|
|
1662
|
+
// server-side, not gone, and its draft has to wait for Save. Keyed on the
|
|
1663
|
+
// rows as shown, which is what the core row model is built from.
|
|
1664
|
+
// Through the ref, not `table`: `useTable` hands out a fresh copy of the
|
|
1665
|
+
// table every render, and listing it would run this on every one.
|
|
1666
|
+
const editingOn = editing !== undefined;
|
|
1667
|
+
const serverSideRows =
|
|
1668
|
+
options.manualPagination === true || options.manualFiltering === true;
|
|
1669
|
+
useEffect(() => {
|
|
1670
|
+
if (!editingOn || serverSideRows) return;
|
|
1671
|
+
engine.forgetMissingRows(tableRef.current.getCoreRowModel().rowsById);
|
|
1672
|
+
}, [shown.rows, editingOn, serverSideRows, engine]);
|
|
1673
|
+
|
|
1674
|
+
// Readers that hold the table and nothing else - `exportGrid`,
|
|
1675
|
+
// `buildGridCellMatrix` - still leave a deletion-marked row out. Keyed on
|
|
1676
|
+
// the store, which every copy of the table shares, so once is enough.
|
|
1677
|
+
useEffect(
|
|
1678
|
+
() => registerDeletedRows(tableRef.current, engine.isRowDeleted),
|
|
1679
|
+
[engine],
|
|
1680
|
+
);
|
|
1133
1681
|
|
|
1134
1682
|
// Editing without stable ids points every draft at whatever record slides
|
|
1135
1683
|
// into that index after a sort. Loud, once, in development.
|
|
@@ -1143,55 +1691,72 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1143
1691
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1144
1692
|
}, []);
|
|
1145
1693
|
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1694
|
+
// A controlled slice without its `onXChange` cannot change: TanStack routes
|
|
1695
|
+
// every write through the callback, and the next options sync restores the
|
|
1696
|
+
// consumer's value. Warned once in development; `initialState` is the option
|
|
1697
|
+
// for a starting value.
|
|
1698
|
+
useEffect(() => {
|
|
1699
|
+
for (const { slice, handler } of findFrozenStateSlices(options)) {
|
|
1700
|
+
console.warn(
|
|
1701
|
+
`TMDataGrid: state.${slice} is controlled but no ${handler} was passed - the slice cannot change. Add ${handler}, or use initialState.${slice} for a starting value.`,
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
// A mount-time contract, like the check above.
|
|
1705
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1706
|
+
}, []);
|
|
1707
|
+
|
|
1708
|
+
// The tree column appears with the first grouped column and goes away with
|
|
1709
|
+
// the last, so an ungrouped grid looks exactly as it did before grouping
|
|
1150
1710
|
// existed. Driven from a subscription rather than by rebuilding the column
|
|
1151
1711
|
// array, because the array is what the table is built from - deriving it from
|
|
1152
1712
|
// table state would close the loop. Visibility is the one column property
|
|
1153
1713
|
// that can be changed after the fact without touching the definitions.
|
|
1154
1714
|
//
|
|
1155
|
-
// Two, and this one is a workaround. In table-core 9.0.0-beta.21 the
|
|
1156
|
-
// per-region column APIs do not list `grouping` among their memo
|
|
1157
|
-
// dependencies, even though they all derive from `getAllLeafColumns()`, which
|
|
1158
|
-
// does:
|
|
1159
|
-
//
|
|
1160
|
-
// | API | Declares |
|
|
1161
|
-
// | --- | --- |
|
|
1162
|
-
// | `getLeft/Center/RightVisibleLeafColumns` | columns, columnPinning, columnVisibility, columnOrder |
|
|
1163
|
-
// | `getLeft/Center/RightHeaderGroups` | columnPinning, columnOrder |
|
|
1164
|
-
// | `row.getLeft/Center/RightVisibleCells` | columnPinning, columnVisibility |
|
|
1165
|
-
//
|
|
1166
|
-
// So grouping a *second* column leaves every one of them returning the
|
|
1167
|
-
// previous list: the column TanStack removed keeps its header and its grid
|
|
1168
|
-
// track, and the row cells no longer line up with them. The first grouping
|
|
1169
|
-
// appears to work only because the visibility write above happens to touch a
|
|
1170
|
-
// dependency they share.
|
|
1171
|
-
//
|
|
1172
|
-
// Re-publishing `columnVisibility` and `columnOrder` - same contents, new
|
|
1173
|
-
// identity - invalidates all three families. `columnOrder` is the only
|
|
1174
|
-
// dependency the header groups declare, and `columnVisibility` the only one
|
|
1175
|
-
// the cells do, so both are needed. Remove this once the deps are fixed
|
|
1176
|
-
// upstream; the test that fails without it groups two columns and asserts the
|
|
1177
|
-
// second one leaves the grid.
|
|
1178
|
-
//
|
|
1179
1715
|
// Writing back into the store from its own subscriber is safe: the guard is
|
|
1180
|
-
// on `grouping`'s identity, and
|
|
1181
|
-
//
|
|
1716
|
+
// on `grouping`'s identity, and the write does not touch it, so the callback
|
|
1717
|
+
// it triggers short-circuits.
|
|
1182
1718
|
useEffect(() => {
|
|
1183
1719
|
if (!groupColumnEnabled) return;
|
|
1184
1720
|
let previousGrouping = table.store.state.grouping;
|
|
1721
|
+
// Update the ref before any write: it feeds the visibility injection
|
|
1722
|
+
// above, and a write below re-renders the hook. With a stale ref the
|
|
1723
|
+
// injection would restore the old value on every render. This also
|
|
1724
|
+
// corrects the mount value when an external atom owns `grouping`, which
|
|
1725
|
+
// the ref's initializer cannot read.
|
|
1726
|
+
groupingActiveRef.current = previousGrouping.length > 0;
|
|
1727
|
+
|
|
1728
|
+
// The tree column's entry in `initialState` only reaches a slice the
|
|
1729
|
+
// table owns. With `columnVisibility` in an external atom the entry is
|
|
1730
|
+
// missing, and a missing entry means visible: the tree column would
|
|
1731
|
+
// render in an ungrouped grid. The entry is seeded here through the table
|
|
1732
|
+
// API instead, so the write reaches whichever store owns the slice. No-op
|
|
1733
|
+
// when the entry is already correct. `?.`: an external atom can hold
|
|
1734
|
+
// `undefined`.
|
|
1735
|
+
if (
|
|
1736
|
+
table.store.state.columnVisibility?.[GROUP_COLUMN_ID] !==
|
|
1737
|
+
(previousGrouping.length > 0)
|
|
1738
|
+
) {
|
|
1739
|
+
table.setColumnVisibility((old) => ({
|
|
1740
|
+
...old,
|
|
1741
|
+
[GROUP_COLUMN_ID]: previousGrouping.length > 0,
|
|
1742
|
+
}));
|
|
1743
|
+
}
|
|
1185
1744
|
|
|
1186
1745
|
const subscription = table.store.subscribe((state) => {
|
|
1187
1746
|
if (state.grouping === previousGrouping) return;
|
|
1188
1747
|
previousGrouping = state.grouping;
|
|
1748
|
+
// Keeps the entry injected into a controlled `columnVisibility` (see
|
|
1749
|
+
// requestedState) in sync with grouping.
|
|
1750
|
+
groupingActiveRef.current = state.grouping.length > 0;
|
|
1751
|
+
// On a controlled slice the write below round-trips through the
|
|
1752
|
+
// consumer's handler; the next render must forward it unstabilized.
|
|
1753
|
+
// See republishControlledStateRef.
|
|
1754
|
+
republishControlledStateRef.current = true;
|
|
1189
1755
|
|
|
1190
1756
|
table.setColumnVisibility((old) => ({
|
|
1191
1757
|
...old,
|
|
1192
1758
|
[GROUP_COLUMN_ID]: state.grouping.length > 0,
|
|
1193
1759
|
}));
|
|
1194
|
-
table.setColumnOrder((old) => [...old]);
|
|
1195
1760
|
});
|
|
1196
1761
|
|
|
1197
1762
|
return () => subscription.unsubscribe();
|
|
@@ -1201,10 +1766,11 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1201
1766
|
// from an effect on a state snapshot) means nothing is missed, including
|
|
1202
1767
|
// changes made straight through the table API by the consumer.
|
|
1203
1768
|
//
|
|
1204
|
-
// Writes are debounced because
|
|
1205
|
-
//
|
|
1206
|
-
// synchronously on the main thread. The
|
|
1207
|
-
// has to agree with the table once the
|
|
1769
|
+
// Writes are debounced because a state change can arrive on every pointer
|
|
1770
|
+
// move - a resize drag under `columnResizeMode: "onChange"`, a range
|
|
1771
|
+
// selection - and `setItem` serialises synchronously on the main thread. The
|
|
1772
|
+
// trailing edge is enough: storage only has to agree with the table once the
|
|
1773
|
+
// user stops.
|
|
1208
1774
|
useEffect(() => {
|
|
1209
1775
|
if (!hasPersistenceKeys(persist)) return;
|
|
1210
1776
|
writePersistedState(table.store.state, persist);
|
|
@@ -1235,9 +1801,11 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1235
1801
|
|
|
1236
1802
|
const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
|
|
1237
1803
|
{
|
|
1238
|
-
|
|
1239
|
-
|
|
1804
|
+
// `useCreateStore` builds the store once per mount, so `defaultOpen` is
|
|
1805
|
+
// read the way `initialState` is - a starting point, not a controller.
|
|
1806
|
+
filterPanelOpen: filters.defaultOpen,
|
|
1240
1807
|
filterPanelColumnId: null,
|
|
1808
|
+
headerFilterColumnId: null,
|
|
1241
1809
|
draggedColumnId: null,
|
|
1242
1810
|
// `useCreateStore` builds the store once per mount, so this is a genuine
|
|
1243
1811
|
// default rather than a value that would fight later clicks.
|
|
@@ -1245,6 +1813,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1245
1813
|
selectionAnchorRowId: null,
|
|
1246
1814
|
focusedCell: null,
|
|
1247
1815
|
cellRange: null,
|
|
1816
|
+
exportPicker: null,
|
|
1248
1817
|
},
|
|
1249
1818
|
({ setState }) => ({
|
|
1250
1819
|
openFilterPanel: (columnId = null) =>
|
|
@@ -1259,10 +1828,14 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1259
1828
|
filterPanelOpen: false,
|
|
1260
1829
|
filterPanelColumnId: null,
|
|
1261
1830
|
})),
|
|
1262
|
-
|
|
1263
|
-
setState((prev) => ({ ...prev,
|
|
1264
|
-
|
|
1265
|
-
setState((prev) => ({ ...prev,
|
|
1831
|
+
openExportPicker: (request) =>
|
|
1832
|
+
setState((prev) => ({ ...prev, exportPicker: request })),
|
|
1833
|
+
closeExportPicker: () =>
|
|
1834
|
+
setState((prev) => ({ ...prev, exportPicker: null })),
|
|
1835
|
+
focusPanelFilter: (columnId) =>
|
|
1836
|
+
setState((prev) => ({ ...prev, filterPanelColumnId: columnId })),
|
|
1837
|
+
focusHeaderFilter: (columnId) =>
|
|
1838
|
+
setState((prev) => ({ ...prev, headerFilterColumnId: columnId })),
|
|
1266
1839
|
startColumnDrag: (columnId) =>
|
|
1267
1840
|
setState((prev) => ({ ...prev, draggedColumnId: columnId })),
|
|
1268
1841
|
endColumnDrag: () =>
|
|
@@ -1351,12 +1924,12 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1351
1924
|
table.setColumnSizing({ ...initial?.columnSizing });
|
|
1352
1925
|
table.setColumnOrder([...(initial?.columnOrder ?? [])]);
|
|
1353
1926
|
table.setColumnPinning({
|
|
1354
|
-
|
|
1927
|
+
start: [
|
|
1355
1928
|
...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
|
|
1356
1929
|
...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
1357
1930
|
...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
|
|
1358
1931
|
...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
|
|
1359
|
-
...(initial?.columnPinning?.
|
|
1932
|
+
...(initial?.columnPinning?.start ?? []).filter(
|
|
1360
1933
|
(id) =>
|
|
1361
1934
|
id !== ROW_NUMBER_COLUMN_ID &&
|
|
1362
1935
|
id !== SELECT_COLUMN_ID &&
|
|
@@ -1364,8 +1937,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1364
1937
|
id !== GROUP_COLUMN_ID,
|
|
1365
1938
|
),
|
|
1366
1939
|
],
|
|
1367
|
-
|
|
1368
|
-
...(initial?.columnPinning?.
|
|
1940
|
+
end: [
|
|
1941
|
+
...(initial?.columnPinning?.end ?? []).filter(
|
|
1369
1942
|
(id) => id !== EDIT_COLUMN_ID,
|
|
1370
1943
|
),
|
|
1371
1944
|
...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
|
|
@@ -1388,6 +1961,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1388
1961
|
ui,
|
|
1389
1962
|
edit,
|
|
1390
1963
|
features,
|
|
1964
|
+
filters,
|
|
1965
|
+
exportOptions,
|
|
1391
1966
|
labels,
|
|
1392
1967
|
renderDetails,
|
|
1393
1968
|
renderDetailsEstHeight,
|
|
@@ -1399,20 +1974,44 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1399
1974
|
}
|
|
1400
1975
|
|
|
1401
1976
|
/**
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1977
|
+
* Gives a column an empty filter of its default operator, unless it already
|
|
1978
|
+
* has one - which is what makes a surface open on a row rather than on
|
|
1979
|
+
* nothing.
|
|
1980
|
+
*
|
|
1981
|
+
* @internal Shared by `openColumnFilter` and the toolbar's filter button.
|
|
1404
1982
|
*/
|
|
1405
|
-
export function
|
|
1983
|
+
export function seedColumnFilter<TData extends RowData>(
|
|
1406
1984
|
api: TMDataGridApi<TData>,
|
|
1407
1985
|
columnId: string,
|
|
1408
1986
|
): void {
|
|
1409
1987
|
const column = api.table.getColumn(columnId);
|
|
1410
|
-
if (column
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1988
|
+
if (column === undefined || column.getFilterValue() !== undefined) return;
|
|
1989
|
+
const operator = getColumnDefaultOperator(column);
|
|
1990
|
+
column.setFilterValue({ operator, value: emptyValueForOperator(operator) });
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Sends the user to a column's filter control, seeding an empty filter when
|
|
1995
|
+
* the column has none yet - what "Filter" in the column menu and a click on a
|
|
1996
|
+
* filter pill both do.
|
|
1997
|
+
*
|
|
1998
|
+
* Which control that is follows the grid's `filters` option. Under
|
|
1999
|
+
* `inHeader` it is the column's header control, which is already on screen, so
|
|
2000
|
+
* the call focuses it and leaves the popup or sidebar closed. Otherwise it is
|
|
2001
|
+
* the panel's row for that column, and the call opens the surface on it.
|
|
2002
|
+
*/
|
|
2003
|
+
export function openColumnFilter<TData extends RowData>(
|
|
2004
|
+
api: TMDataGridApi<TData>,
|
|
2005
|
+
columnId: string,
|
|
2006
|
+
): void {
|
|
2007
|
+
seedColumnFilter(api, columnId);
|
|
2008
|
+
if (api.filters.inHeader) {
|
|
2009
|
+
// The header control is always visible, so there is nothing to open -
|
|
2010
|
+
// only a column to point at. The header row watching this focuses it and
|
|
2011
|
+
// scrolls it into view. The panel, if one is also showing, is left alone:
|
|
2012
|
+
// it reads the other slot.
|
|
2013
|
+
api.ui.actions.focusHeaderFilter(columnId);
|
|
2014
|
+
return;
|
|
1416
2015
|
}
|
|
1417
2016
|
api.ui.actions.openFilterPanel(columnId);
|
|
1418
2017
|
}
|