@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.
Files changed (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /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 TMDataGridEditCommitDraftsArgs,
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 filter starts with, and the
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
- columnsPanelOpen: boolean;
298
- /** Column whose filter row should be focused when the panel opens. */
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
- setColumnsPanelOpen: (open: boolean) => void;
344
- toggleColumnsPanel: () => void;
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, so a drawer
391
- * or detail panel can share a row's draft. Inert until `editing` is set.
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 ✓ under the
478
- * immediate modes, `submitAll` under draft. Create the record and let it
479
- * arrive back through `data`; the engine's `tempId` never leaves the grid.
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` under the immediate modes - confirmation, if
484
- * any, belongs in here. Under draft, deletions accumulate in
485
- * `edit.state.deletedRowIds` instead and are reported by `submitAll`.
486
- * Setting this also puts the trash can in the edit lane.
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. `mode` picks what counts as a commit and which
494
- * controls trigger it; the other members act within that mode.
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
- * | Mode | Commit | Cancel |
497
- * | ---- | ------ | ------ |
498
- * | `"cell"` | Enter, Tab, blur - Sheets | Escape |
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
- * `onCommitDrafts` exists only under `mode: "draft"`, the one mode whose
506
- * `submitAll` calls it.
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
- * Draft mode's save, called once by `edit.submitAll()` with every
524
- * valid dirty row. Without it, `submitAll` falls back to the per-row
525
- * {@link TMDataGridEditingCallbacks.onCommit} loop. Rows failing
526
- * validation stay open either way; a rejection keeps every draft.
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: TMDataGridEditCommitDraftsArgs<TData>,
530
- ) => void | Promise<void>;
698
+ args: TMDataGridSaveDraftsArgs<TData>,
699
+ ) =>
700
+ | void
701
+ | TMDataGridSaveDraftsResult
702
+ | Promise<void | TMDataGridSaveDraftsResult>;
531
703
  /**
532
- * Keep confirmed entry rows pinned in the sticky entry block until
533
- * Save all. Off by default: a confirmed row joins the scrolling
534
- * flow above the body rows instead - the block a row is *typed*
535
- * into is always sticky, but entered rows scroll, so entering many
536
- * cannot fill the viewport with sticky chrome.
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
- mode: Exclude<TMDataGridEditMode, "draft">;
542
- /** Only `"draft"`'s `submitAll` ever calls it - see the other branch. */
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
- /** Confirmed entry rows exist only under `"draft"` - see there. */
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 mode, where it is the change marker and the per-row
925
- // revert - with or without `onCommitDrafts`, since the per-row `submitAll`
926
- // fallback is a first-class configuration.
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
- // Draft mode's lane holds three controls (state icon, undo, trash) where
943
- // the other modes hold two - it gets the wider track.
944
- const editIsDraftMode = editing?.mode === "draft";
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, editIsDraftMode)]
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
- editIsDraftMode,
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
- columnResizeMode: "onChange",
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
- left: [
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?.left ??
1069
- options.initialState?.columnPinning?.left ??
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
- right: [
1602
+ end: [
1082
1603
  ...(
1083
- persistedState.columnPinning?.right ??
1084
- options.initialState?.columnPinning?.right ??
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 edit engine. Built once per mount; everything it needs later - 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
- onEditCommitDrafts:
1113
- editing?.onCommitDrafts as TMDataGridEditEngineContext["onEditCommitDrafts"],
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 modes mid-flight drops every draft: the policies disagree about
1125
- // what an open form means, and carrying one across is how a draft parked
1126
- // under "draft" silently commits under "cell".
1127
- const previousEditModeRef = useRef(editMode);
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
- if (previousEditModeRef.current === editMode) return;
1130
- previousEditModeRef.current = editMode;
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
- // Two things have to happen whenever `grouping` changes.
1147
- //
1148
- // One: the tree column appears with the first grouped column and goes away
1149
- // with the last, so an ungrouped grid looks exactly as it did before grouping
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 neither write touches it, so the callback
1181
- // these writes trigger short-circuits.
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 `columnResizeMode: "onChange"` publishes a new
1205
- // state on every pointer move of a resize drag, and `setItem` serialises
1206
- // synchronously on the main thread. The trailing edge is enough: storage only
1207
- // has to agree with the table once the user stops.
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
- filterPanelOpen: false,
1239
- columnsPanelOpen: false,
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
- setColumnsPanelOpen: (open) =>
1263
- setState((prev) => ({ ...prev, columnsPanelOpen: open })),
1264
- toggleColumnsPanel: () =>
1265
- setState((prev) => ({ ...prev, columnsPanelOpen: !prev.columnsPanelOpen })),
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
- left: [
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?.left ?? []).filter(
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
- right: [
1368
- ...(initial?.columnPinning?.right ?? []).filter(
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
- * Opens the filter panel for a column, seeding an empty filter row when the
1403
- * column has none yet - mirrors "Filter" in the column header menu.
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 openColumnFilter<TData extends RowData>(
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 && column.getFilterValue() === undefined) {
1411
- const operator = getColumnDefaultOperator(column);
1412
- column.setFilterValue({
1413
- operator,
1414
- value: emptyValueForOperator(operator),
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
  }