@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1323 -796
  3. package/dist/index.js +4719 -3193
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +268 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +69 -78
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +83 -50
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +31 -23
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +8 -8
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  63. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  64. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  65. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
  66. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  68. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  70. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
  71. package/src/components/TMDataGridEntryRows.tsx +354 -0
  72. package/src/components/TMDataGridExportPicker.module.css +77 -0
  73. package/src/components/TMDataGridExportPicker.tsx +234 -0
  74. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  75. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  76. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  77. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  78. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  80. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
  81. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  83. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  84. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  85. package/src/components/TMDataGridMenu.tsx +357 -0
  86. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
  87. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
  89. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  90. package/src/components/TMDataGridToolbar.tsx +181 -0
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  98. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  99. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  100. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  101. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  103. package/src/components/filters/controlLayout.ts +32 -0
  104. package/src/components/filters/filterControlFor.ts +65 -0
  105. package/src/components/generatedColumns.tsx +187 -0
  106. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  107. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  108. package/src/components/useHideableColumns.ts +52 -0
  109. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  110. package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
  111. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  112. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  113. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  114. package/src/core/controlledStateSync.ts +108 -0
  115. package/src/core/deletedRows.ts +34 -0
  116. package/src/core/dom.ts +74 -0
  117. package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
  118. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  119. package/src/core/export.ts +704 -0
  120. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  121. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  122. package/src/core/filterSurface.ts +99 -0
  123. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  124. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  125. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  126. package/src/core/pageReset.ts +120 -0
  127. package/src/core/pagination.ts +81 -0
  128. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  129. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  130. package/src/{tmdatagrid/index.ts → index.ts} +70 -36
  131. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
  132. package/src/useTMDataGridExport.ts +78 -0
  133. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
  134. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  135. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  136. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  137. package/src/tmdatagrid/core/cellExport.ts +0 -320
  138. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  141. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  142. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  143. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  144. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -1,7 +1,12 @@
1
- import { useCreateStore } from "@tanstack/react-store";
1
+ import { useCreateStore, useSelector } from "@tanstack/react-store";
2
+ import { shallow } from "@tanstack/store";
2
3
  import {
4
+ type AccessorFn,
5
+ type AccessorFnColumnDef,
6
+ type AccessorKeyColumnDef,
3
7
  aggregationFns,
4
8
  type ColumnDef,
9
+ type ColumnHelper,
5
10
  columnFacetingFeature,
6
11
  columnFilteringFeature,
7
12
  columnGroupingFeature,
@@ -18,11 +23,17 @@ import {
18
23
  createFilteredRowModel,
19
24
  createGroupedRowModel,
20
25
  createPaginatedRowModel,
26
+ type DeepKeys,
27
+ type DeepValue,
28
+ type DisplayColumnDef,
21
29
  filterFns,
30
+ type GroupColumnDef,
22
31
  globalFilteringFeature,
32
+ type IdentifiedColumnDef,
23
33
  metaHelper,
24
34
  type Row,
25
35
  type RowData,
36
+ rowAggregationFeature,
26
37
  rowExpandingFeature,
27
38
  rowPaginationFeature,
28
39
  rowPinningFeature,
@@ -52,7 +63,7 @@ import {
52
63
  type TMDataGridEditApi,
53
64
  type TMDataGridEditCommitArgs,
54
65
  type TMDataGridSaveDraftsArgs,
55
- type TMDataGridSaveDraftsResult,
66
+ type TMDataGridSaveDraftsResponse,
56
67
  type TMDataGridEditEngineContext,
57
68
  type TMDataGridColumnEditOptions,
58
69
  type TMDataGridEditMode,
@@ -68,6 +79,11 @@ import {
68
79
  } from "./core/filterOperators";
69
80
  import { getColumnDefaultOperator, isControlColumn } from "./core/columnUtils";
70
81
  import type { TMDataGridColumnFilterOptions } from "./core/filterControls";
82
+ import {
83
+ resolveFilterOptions,
84
+ type TMDataGridFiltersOptions,
85
+ type TMDataGridFiltersSettings,
86
+ } from "./core/filterSurface";
71
87
  import {
72
88
  createFuzzyRankedSortedRowModel,
73
89
  fuzzyGlobalFilterFn,
@@ -100,23 +116,34 @@ import {
100
116
  stabilizeControlledState,
101
117
  withoutUndefinedSlices,
102
118
  } from "./core/controlledState";
103
- import type { TMDataGridCellRange } from "./core/cellRange";
104
119
  import {
105
- createSelectColumn,
106
- SELECT_COLUMN_ID,
107
- } from "./components/TMDataGridSelectColumn";
120
+ beginControlledStateSync,
121
+ deferControlledStateSyncPublishes,
122
+ endControlledStateSync,
123
+ } from "./core/controlledStateSync";
124
+ import { registerDeletedRows } from "./core/deletedRows";
108
125
  import {
109
- createGroupColumn,
110
- GROUP_COLUMN_ID,
111
- } from "./components/TMDataGridGroupColumn";
126
+ withPageReset,
127
+ type TMDataGridQueryTable,
128
+ } from "./core/pageReset";
129
+ import type { TMDataGridCellRange } from "./core/cellRange";
112
130
  import {
113
- createDetailsColumn,
114
- DETAILS_COLUMN_ID,
115
- } from "./components/TMDataGridDetailsColumn";
131
+ resolveExportOptions,
132
+ type TMDataGridExportOptions,
133
+ type TMDataGridExportPickerRequest,
134
+ type TMDataGridExportSettings,
135
+ type TMDataGridExportValueGetter,
136
+ } from "./core/export";
137
+ import { SELECT_COLUMN_ID } from "./components/TMDataGridSelectColumn";
138
+ import { GROUP_COLUMN_ID } from "./components/TMDataGridGroupColumn";
139
+ import { DETAILS_COLUMN_ID } from "./components/TMDataGridDetailsColumn";
140
+ import { EDIT_COLUMN_ID } from "./components/TMDataGridEditColumn";
116
141
  import {
142
+ createDetailsColumn,
117
143
  createEditColumn,
118
- EDIT_COLUMN_ID,
119
- } from "./components/TMDataGridEditColumn";
144
+ createGroupColumn,
145
+ createSelectColumn,
146
+ } from "./components/generatedColumns";
120
147
  import {
121
148
  createRowNumberColumn,
122
149
  ROW_NUMBER_COLUMN_ID,
@@ -138,8 +165,11 @@ const PERSIST_DEBOUNCE_MS = 200;
138
165
  * `type` and `options` are read by both stages, so one declaration of each
139
166
  * feeds the filter panel and the cell editor, which is why they sit outside
140
167
  * both namespaces.
168
+ *
169
+ * `TData` types the row that `options` and `edit.enabled` callbacks receive.
170
+ * `createTMDataGridColumnHelper<TData>()` fills it in.
141
171
  */
142
- export type TMDataGridColumnMeta = {
172
+ export type TMDataGridColumnMeta<TData extends RowData = TMDataGridRowData> = {
143
173
  /** Name shown in menus and the column manager. Falls back to a string header. */
144
174
  label?: string;
145
175
  /**
@@ -154,7 +184,7 @@ export type TMDataGridColumnMeta = {
154
184
  * function of the table, column and, for editors, the row. See
155
185
  * {@link TMDataGridOptionsSource}.
156
186
  */
157
- options?: TMDataGridOptionsSource;
187
+ options?: TMDataGridOptionsSource<TData>;
158
188
  /** Share of the leftover width this column claims. Defaults to `1`. */
159
189
  flex?: number;
160
190
  align?: "left" | "right" | "center";
@@ -171,8 +201,8 @@ export type TMDataGridColumnMeta = {
171
201
  */
172
202
  enableOrdering?: boolean;
173
203
  /**
174
- * How this column filters: the operator a fresh filter starts with, and the
175
- * value control the filter panel renders for it.
204
+ * How this column filters: which operators it offers, the operator a fresh
205
+ * filter starts with, and the value control the filter panel renders for it.
176
206
  *
177
207
  * ```tsx
178
208
  * meta: {
@@ -198,7 +228,26 @@ export type TMDataGridColumnMeta = {
198
228
  *
199
229
  * See {@link TMDataGridColumnEditOptions}.
200
230
  */
201
- edit?: TMDataGridColumnEditOptions;
231
+ edit?: TMDataGridColumnEditOptions<TData>;
232
+ /**
233
+ * `false` leaves the column out of every export and out of Ctrl+C - for a
234
+ * column of buttons, or one whose value means nothing outside the grid.
235
+ * Defaults to `true`.
236
+ */
237
+ enableExport?: boolean;
238
+ /**
239
+ * The value an export writes for this column, in place of
240
+ * `row.getValue(column.id)`. The export otherwise writes the value, never
241
+ * what the cell renders, so this is where a status code becomes its label
242
+ * or a nested object becomes one field.
243
+ *
244
+ * ```tsx
245
+ * meta: {
246
+ * exportValue: ({ value }) => STATUS_LABELS[value as Status],
247
+ * }
248
+ * ```
249
+ */
250
+ exportValue?: TMDataGridExportValueGetter;
202
251
  };
203
252
 
204
253
  /** Grid-wide configuration passed through `options.meta`. */
@@ -231,6 +280,7 @@ export const tmDataGridFeatures = tableFeatures({
231
280
  columnResizingFeature,
232
281
  columnFacetingFeature,
233
282
  columnGroupingFeature,
283
+ rowAggregationFeature,
234
284
  // Registered for grouping's sake rather than for tree data: the grouped row
235
285
  // model builds the parent rows, and this is what flattens the expanded ones
236
286
  // back into the flat list the body virtualizes.
@@ -270,8 +320,57 @@ export type TMDataGridTable<TData extends RowData> = Table<
270
320
  TData
271
321
  >;
272
322
 
273
- export function createTMDataGridColumnHelper<TData extends RowData>() {
274
- return createColumnHelper<TMDataGridFeatures, TData>();
323
+ /** A column definition with `meta` typed against the row type. */
324
+ type WithRowTypedMeta<TDef, TData extends RowData> = TDef extends unknown
325
+ ? Omit<TDef, "meta"> & { meta?: TMDataGridColumnMeta<TData> }
326
+ : never;
327
+
328
+ /**
329
+ * TanStack's column helper with `meta` typed against `TData`, so the
330
+ * `meta.options` and `meta.edit.enabled` callbacks receive
331
+ * `Row<TMDataGridFeatures, TData>`.
332
+ */
333
+ export type TMDataGridColumnHelper<TData extends RowData> = {
334
+ accessor: <
335
+ TAccessor extends AccessorFn<TData> | DeepKeys<TData>,
336
+ TValue extends TAccessor extends AccessorFn<TData, infer TReturn>
337
+ ? TReturn
338
+ : TAccessor extends DeepKeys<TData>
339
+ ? DeepValue<TData, TAccessor>
340
+ : never,
341
+ >(
342
+ accessor: TAccessor,
343
+ column: WithRowTypedMeta<
344
+ TAccessor extends AccessorFn<TData>
345
+ ? DisplayColumnDef<TMDataGridFeatures, TData, TValue>
346
+ : IdentifiedColumnDef<TMDataGridFeatures, TData, TValue>,
347
+ TData
348
+ >,
349
+ ) => TAccessor extends AccessorFn<TData>
350
+ ? AccessorFnColumnDef<TMDataGridFeatures, TData, TValue>
351
+ : AccessorKeyColumnDef<TMDataGridFeatures, TData, TValue>;
352
+ columns: ColumnHelper<TMDataGridFeatures, TData>["columns"];
353
+ display: (
354
+ column: WithRowTypedMeta<DisplayColumnDef<TMDataGridFeatures, TData>, TData>,
355
+ ) => DisplayColumnDef<TMDataGridFeatures, TData, unknown>;
356
+ group: (
357
+ column: WithRowTypedMeta<
358
+ GroupColumnDef<TMDataGridFeatures, TData, unknown>,
359
+ TData
360
+ >,
361
+ ) => GroupColumnDef<TMDataGridFeatures, TData, unknown>;
362
+ };
363
+
364
+ export function createTMDataGridColumnHelper<
365
+ TData extends RowData,
366
+ >(): TMDataGridColumnHelper<TData> {
367
+ // Same runtime helper. The features object registers one meta type for
368
+ // every table, so the row-typed meta exists only in this signature; the
369
+ // grid calls the callbacks with the rows of the table they belong to.
370
+ return createColumnHelper<
371
+ TMDataGridFeatures,
372
+ TData
373
+ >() as unknown as TMDataGridColumnHelper<TData>;
275
374
  }
276
375
 
277
376
  /** What the `renderDetails` render prop is handed for an expanded row. */
@@ -294,6 +393,9 @@ const DEFAULT_DETAILS_EST_HEIGHT = 160;
294
393
  /** Rows kept mounted on each side of the viewport. See `overscan`. */
295
394
  const DEFAULT_OVERSCAN = 6;
296
395
 
396
+ /** One empty array, so a selector answering "none" never changes identity. */
397
+ const EMPTY_IDS: ReadonlyArray<string> = [];
398
+
297
399
  /**
298
400
  * Chrome state that is *not* table state: which panels are open, and which
299
401
  * column opened the filter panel. Kept in a TanStack Store so consumers can
@@ -301,9 +403,20 @@ const DEFAULT_OVERSCAN = 6;
301
403
  */
302
404
  export type TMDataGridUiState = {
303
405
  filterPanelOpen: boolean;
304
- columnsPanelOpen: boolean;
305
- /** Column whose filter row should be focused when the panel opens. */
406
+ /**
407
+ * Column whose *panel* row should take the focus. Cleared once the row has
408
+ * taken it, so pointing at the same column twice focuses twice.
409
+ */
306
410
  filterPanelColumnId: string | null;
411
+ /**
412
+ * Column whose *header filter* control should take the focus, under
413
+ * `filters.inHeader`. Cleared once taken, like the one above.
414
+ *
415
+ * Its own slot rather than a second reader of `filterPanelColumnId`: a grid
416
+ * can have header filters and a panel at once, and two controls racing to
417
+ * answer one id means whichever mounted last wins the caret.
418
+ */
419
+ headerFilterColumnId: string | null;
307
420
  /**
308
421
  * Column being dragged by its header, if any. Held here rather than read from
309
422
  * `dataTransfer`, which browsers keep unreadable until the drop.
@@ -342,13 +455,33 @@ export type TMDataGridUiState = {
342
455
  * describe different places.
343
456
  */
344
457
  cellRange: TMDataGridCellRange | null;
458
+ /**
459
+ * The export column picker, while it is open: which rows it exports and the
460
+ * options of the item that opened it. `null` while closed. Held here rather
461
+ * than in the menu item, which unmounts with the dropdown the moment it is
462
+ * clicked.
463
+ */
464
+ exportPicker: TMDataGridExportPickerRequest | null;
345
465
  };
346
466
 
347
467
  export type TMDataGridUiActions = {
348
468
  openFilterPanel: (columnId?: string | null) => void;
349
469
  closeFilterPanel: () => void;
350
- setColumnsPanelOpen: (open: boolean) => void;
351
- toggleColumnsPanel: () => void;
470
+ /** Opens the export column picker for `request`. See `TMDataGrid.Menu.Export`'s `columns="custom"`. */
471
+ openExportPicker: (request: TMDataGridExportPickerRequest) => void;
472
+ closeExportPicker: () => void;
473
+ /**
474
+ * Points at a column's row in the filter panel without opening anything.
475
+ * `openFilterPanel` does this as well as opening; this is the half a panel
476
+ * that is already showing needs.
477
+ */
478
+ focusPanelFilter: (columnId: string | null) => void;
479
+ /**
480
+ * Points at a column's header filter control - what `openColumnFilter` does
481
+ * under `filters.inHeader`, where there is no panel to open. The header row
482
+ * scrolls the column into view and focuses it.
483
+ */
484
+ focusHeaderFilter: (columnId: string | null) => void;
352
485
  startColumnDrag: (columnId: string) => void;
353
486
  endColumnDrag: () => void;
354
487
  /**
@@ -393,13 +526,28 @@ export type TMDataGridApi<TData extends RowData> = {
393
526
  ui: TMDataGridUiStore;
394
527
  /**
395
528
  * The edit engine - open forms, dirty/error projections, and the verbs
396
- * (`begin`, `commit`, `cancel`, `submitAll`). `edit.getForm(rowId)` hands
397
- * out the same TanStack Form the inline editors write through, so a drawer
398
- * or detail panel can share a row's draft. Inert until `editing` is set.
529
+ * (`begin`, `commit`, `cancel`, `saveDrafts`). `edit.getForm(rowId)` hands
530
+ * out the same TanStack Form the inline editors write through while a row
531
+ * is open, so a drawer or detail panel can share a row's draft; a
532
+ * committed row has no form until `begin` reopens it. Inert until
533
+ * `editing` is set.
399
534
  */
400
535
  edit: TMDataGridEditApi<TData>;
401
536
  /** Table-level feature switches, re-read from options on every render. */
402
537
  features: TMDataGridFeatureFlags;
538
+ /**
539
+ * Where the filter controls live, the `filters` option with its defaults
540
+ * filled in. On the api rather than in a component's props because the
541
+ * pills, the column menu and `openColumnFilter` all have to agree with the
542
+ * table about which surface is on.
543
+ */
544
+ filters: TMDataGridFiltersSettings;
545
+ /**
546
+ * How the grid exports, the `exportOptions` option with its defaults filled
547
+ * in. Read by `useTMDataGridExport`, the `TMDataGrid.Menu.Export*` items and
548
+ * the cell-range menu, so every export of the grid agrees on the format.
549
+ */
550
+ exportOptions: TMDataGridExportSettings;
403
551
  /** Every string the chrome renders, `labels` merged over the English defaults. */
404
552
  labels: TMDataGridLabels;
405
553
  /** The detail renderer, when row details are on. See `renderDetails`. */
@@ -588,31 +736,21 @@ export type TMDataGridEditingOptions<TData extends RowData> =
588
736
  *
589
737
  * Rows still open are not in the payload and stay open. Returning
590
738
  * nothing saves the whole store and throwing saves none of it;
591
- * return a {@link TMDataGridSaveDraftsResult} to save part of it.
739
+ * return a {@link TMDataGridSaveDraftsResponse} to save part of it.
592
740
  */
593
741
  onSaveDrafts?: (
594
742
  args: TMDataGridSaveDraftsArgs<TData>,
595
743
  ) =>
596
744
  | void
597
- | TMDataGridSaveDraftsResult
598
- | Promise<void | TMDataGridSaveDraftsResult>;
599
- /**
600
- * @deprecated Renamed to {@link onSaveDrafts} - it fires when the
601
- * draft store is saved, not when a row commits into it. Still
602
- * honoured; removed in a later beta.
603
- */
604
- onCommitDrafts?: (
605
- args: TMDataGridSaveDraftsArgs<TData>,
606
- ) =>
607
- | void
608
- | TMDataGridSaveDraftsResult
609
- | Promise<void | TMDataGridSaveDraftsResult>;
745
+ | TMDataGridSaveDraftsResponse
746
+ | Promise<void | TMDataGridSaveDraftsResponse>;
610
747
  /**
611
- * Keep parked entry rows pinned in the sticky entry block until
612
- * the draft store is saved. Off by default: a parked row joins the
613
- * scrolling flow above the body rows instead - the block a row is
614
- * *typed* into is always sticky, but parked rows scroll, so
615
- * entering many cannot fill the viewport with sticky chrome.
748
+ * Keep committed entry rows pinned in the sticky entry block until
749
+ * the draft store is saved, out of the body's sort. Off by default:
750
+ * a committed row joins the body rows instead, sorted and filtered
751
+ * with them - the block a row is *typed* into is always sticky, but
752
+ * committed rows scroll, so entering many cannot fill the viewport
753
+ * with sticky chrome.
616
754
  */
617
755
  newRowsSticky?: boolean;
618
756
  }
@@ -621,8 +759,6 @@ export type TMDataGridEditingOptions<TData extends RowData> =
621
759
  draft?: false;
622
760
  /** Only `draft: true` has a store to save - see the other branch. */
623
761
  onSaveDrafts?: never;
624
- /** @deprecated See {@link onSaveDrafts}. */
625
- onCommitDrafts?: never;
626
762
  /** Parked entry rows exist only under `draft: true` - see there. */
627
763
  newRowsSticky?: never;
628
764
  }
@@ -688,12 +824,55 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
688
824
  * no extra flag.
689
825
  */
690
826
  enablePagination?: boolean;
827
+ /**
828
+ * Sends the grid back to the first page whenever the query changes - a
829
+ * column filter, the quick search, the sort or the grouping. On by
830
+ * default. TanStack's own `autoResetPageIndex` is switched off by the
831
+ * grid: it fires on any change to the `data` array, which under
832
+ * `editing.draft` is every commit.
833
+ *
834
+ * Server-side, `pageIndex` is a position in a result set the grid does not
835
+ * own: narrowing the query leaves it pointing past the last page, and the
836
+ * next request comes back empty. The reset is applied in the same event as
837
+ * the change, so one request goes out, for the first page of the new query.
838
+ */
839
+ resetPageOnQueryChange?: boolean;
691
840
  /**
692
841
  * The row-number gutter: a generated lane, outermost left, numbering the
693
842
  * rows of the current view - sorted, filtered, continuing across pages,
694
843
  * with group rows unnumbered. Off by default.
695
844
  */
696
845
  enableRowNumbers?: boolean;
846
+ /**
847
+ * Where the grid puts its filter controls - a popup over the rows, a sidebar
848
+ * beside them, controls in the header row, or nowhere at all so you place
849
+ * `TMDataGrid.FilterPanel` yourself.
850
+ *
851
+ * ```tsx
852
+ * useTMDataGrid({ data, columns, filters: { surface: "sidebar", inHeader: true } });
853
+ * ```
854
+ *
855
+ * Defaults to `{ surface: "popup" }` - the floating panel the grid has
856
+ * always shown. See {@link TMDataGridFiltersOptions}.
857
+ *
858
+ * Read field by field, so a literal is fine here - unlike `labels` or
859
+ * `persist`, this one does not have to be referentially stable.
860
+ */
861
+ filters?: TMDataGridFiltersOptions;
862
+ /**
863
+ * How the grid exports: the file format, the file name and whether the
864
+ * column labels go in as the first row. Defaults to `csvExcelFormat()`,
865
+ * `"export"` and `true`. See {@link TMDataGridExportOptions}.
866
+ *
867
+ * ```tsx
868
+ * useTMDataGrid({ data, columns, exportOptions: { format: csvFormat(), fileName: "employees" } });
869
+ * ```
870
+ *
871
+ * Read field by field like `filters`, so a literal is fine. A `format`
872
+ * built inline is rebuilt every render, which costs nothing but a small
873
+ * object; keep it at module scope when that bothers you.
874
+ */
875
+ exportOptions?: TMDataGridExportOptions;
697
876
  /**
698
877
  * How the quick search (`TMDataGrid.Search`) matches. `"fuzzy"` - the
699
878
  * default - forgives typos and skipped characters, and while it is the
@@ -957,6 +1136,7 @@ export function useTMDataGrid<TData extends RowData>({
957
1136
  labels: labelsOverride,
958
1137
  enableColumnOrdering,
959
1138
  enablePagination,
1139
+ resetPageOnQueryChange,
960
1140
  enableRowNumbers,
961
1141
  selectionMode,
962
1142
  showSelectedBackground,
@@ -964,6 +1144,8 @@ export function useTMDataGrid<TData extends RowData>({
964
1144
  onHighlightedRowChange,
965
1145
  cellSelection,
966
1146
  onFocusedCellChange,
1147
+ filters: filterOptions,
1148
+ exportOptions: exportOptionsOverride,
967
1149
  editing,
968
1150
  renderDetails,
969
1151
  renderDetailsEstHeight = DEFAULT_DETAILS_EST_HEIGHT,
@@ -995,6 +1177,50 @@ export function useTMDataGrid<TData extends RowData>({
995
1177
  // Resolved on the override's identity, so a module-scope dictionary costs one
996
1178
  // merge for the lifetime of the grid.
997
1179
  const labels = useMemo(() => mergeLabels(labelsOverride), [labelsOverride]);
1180
+ // Field by field for the same reason as `filters` below.
1181
+ const {
1182
+ format: exportFormat,
1183
+ fileName: exportFileName,
1184
+ includeHeaders: exportIncludeHeaders,
1185
+ columns: exportColumns,
1186
+ } = exportOptionsOverride ?? {};
1187
+ const exportOptions = useMemo(
1188
+ () =>
1189
+ resolveExportOptions({
1190
+ format: exportFormat,
1191
+ fileName: exportFileName,
1192
+ includeHeaders: exportIncludeHeaders,
1193
+ columns: exportColumns,
1194
+ }),
1195
+ [exportFormat, exportFileName, exportIncludeHeaders, exportColumns],
1196
+ );
1197
+ // Unpacked before the memo, so the api is keyed on the five fields rather
1198
+ // than on the object's identity - which is what lets `filters` be written as
1199
+ // a literal, the way it reads best, without republishing every render.
1200
+ const {
1201
+ surface: filterSurface,
1202
+ sidebarSide: filterSidebarSide,
1203
+ sidebarWidth: filterSidebarWidth,
1204
+ defaultOpen: filtersDefaultOpen,
1205
+ inHeader: filtersInHeader,
1206
+ } = filterOptions ?? {};
1207
+ const filters = useMemo(
1208
+ () =>
1209
+ resolveFilterOptions({
1210
+ surface: filterSurface,
1211
+ sidebarSide: filterSidebarSide,
1212
+ sidebarWidth: filterSidebarWidth,
1213
+ defaultOpen: filtersDefaultOpen,
1214
+ inHeader: filtersInHeader,
1215
+ }),
1216
+ [
1217
+ filterSurface,
1218
+ filterSidebarSide,
1219
+ filterSidebarWidth,
1220
+ filtersDefaultOpen,
1221
+ filtersInHeader,
1222
+ ],
1223
+ );
998
1224
 
999
1225
  const pinningEnabled = options.enableColumnPinning !== false;
1000
1226
  const selectColumnEnabled = features.selectColumn;
@@ -1169,12 +1395,129 @@ export function useTMDataGrid<TData extends RowData>({
1169
1395
  [],
1170
1396
  );
1171
1397
 
1398
+ // The edit engine. Built once per mount; everything it needs later - the
1399
+ // table, the mode, the consumer's callbacks - is read through a ref updated
1400
+ // every render, so forms created at `begin()` always call the latest
1401
+ // `onEditCommit` (the onHighlightedRowChangeRef pattern, applied wholesale).
1402
+ // Created ahead of the table because the table's `data` reads its store;
1403
+ // the ref is filled in once the table exists, and the engine only reads it
1404
+ // inside verbs, never while being built.
1405
+ const editContextRef = useRef<TMDataGridEditEngineContext>(null as never);
1406
+ // The engine is erased; the row type comes back on the way out, which is
1407
+ // what makes `edit.addRow(values)` check against `TData`.
1408
+ const [engine] = useState(() =>
1409
+ createEditEngine(() => editContextRef.current),
1410
+ );
1411
+ const edit = engine as unknown as TMDataGridEditApi<TData>;
1412
+
1413
+ // The rows as shown. Under `editing.draft` a committed row is a row like any
1414
+ // other to the table: its draft replaces the consumer's record and a
1415
+ // committed entry row is prepended, so sorting, filtering, grouping and
1416
+ // aggregates all read the draft store's values. Rows still being typed
1417
+ // into are not here - their values are undecided, and re-sorting under a
1418
+ // caret is not something anyone wants. `newRowsSticky` keeps committed
1419
+ // entry rows in the entry block, so those stay out too.
1420
+ //
1421
+ // Everything is keyed on identities the engine keeps stable while nothing
1422
+ // is decided, so a keystroke in an open editor never rebuilds the model.
1423
+ // `committedValues` rather than `committedRowIds`: the snapshot outlives a
1424
+ // reopen, so a parked row being edited again keeps its place until the
1425
+ // next commit or a cancel decides otherwise.
1426
+ const editCommittedValues = useSelector(
1427
+ edit.store,
1428
+ (state) => state.committedValues,
1429
+ );
1430
+ const newRowsSticky = features.editNewRowsSticky;
1431
+ const editCreatedIds = useSelector(
1432
+ edit.store,
1433
+ (state) =>
1434
+ newRowsSticky
1435
+ ? EMPTY_IDS
1436
+ : state.newRows
1437
+ .filter((newRow) => newRow.committed)
1438
+ .map((newRow) => newRow.tempId),
1439
+ { compare: shallow },
1440
+ );
1441
+ // Read through a ref, not listed as a dependency: an inline `getRowId` is
1442
+ // a fresh function every render, and the merged array must keep its
1443
+ // identity across renders or the table rebuilds its row models on each.
1444
+ const consumerGetRowIdRef = useRef(options.getRowId);
1445
+ consumerGetRowIdRef.current = options.getRowId;
1446
+ const shown = useMemo(() => {
1447
+ const consumerGetRowId = consumerGetRowIdRef.current;
1448
+ const idOf = new Map<object, string>();
1449
+ const created: Array<TData> = [];
1450
+ for (const tempId of editCreatedIds) {
1451
+ const values = editCommittedValues[tempId];
1452
+ if (values === undefined) continue;
1453
+ idOf.set(values, tempId);
1454
+ created.push(values as TData);
1455
+ }
1456
+ const snapshots = Object.keys(editCommittedValues).length;
1457
+ if (snapshots === 0) return { rows: options.data, idOf };
1458
+ // Every snapshot may stand in for a record; only the created ones are
1459
+ // known not to. The rest are looked up by the record's own id.
1460
+ const rows =
1461
+ snapshots === created.length
1462
+ ? options.data
1463
+ : options.data.map((record, index) => {
1464
+ const rowId =
1465
+ consumerGetRowId?.(record, index, undefined) ?? String(index);
1466
+ const values = editCommittedValues[rowId];
1467
+ if (values === undefined) return record;
1468
+ idOf.set(values, rowId);
1469
+ return values as TData;
1470
+ });
1471
+ return {
1472
+ rows: created.length === 0 ? rows : [...created, ...rows],
1473
+ idOf,
1474
+ };
1475
+ }, [options.data, editCommittedValues, editCreatedIds]);
1476
+ const shownIdOfRef = useRef(shown.idOf);
1477
+ shownIdOfRef.current = shown.idOf;
1478
+ // A stable id resolver over the merged rows. A draft answers the id of the
1479
+ // record it stands in for - the consumer's `getRowId` never sees a draft
1480
+ // object, so an editable id field cannot rename the row - and a created
1481
+ // row answers its temp id. Everything else is the consumer's own, or
1482
+ // TanStack's index fallback.
1483
+ const shownGetRowId = useCallback(
1484
+ (record: TData, index: number, parent?: Row<TMDataGridFeatures, TData>) =>
1485
+ shownIdOfRef.current.get(record) ??
1486
+ consumerGetRowIdRef.current?.(record, index, parent) ??
1487
+ (parent !== undefined ? `${parent.id}.${index}` : String(index)),
1488
+ [],
1489
+ );
1490
+
1491
+ // Filled immediately after the call below. The query-change wrappers close
1492
+ // over it rather than over `table`, since they are built as part of the
1493
+ // options the table is constructed from.
1494
+ const tableRef = useRef<TMDataGridTable<TData>>(null as never);
1495
+ // A narrower query invalidates the page the grid is on - see pageReset.ts.
1496
+ // On by default everywhere: TanStack's own `autoResetPageIndex` is switched
1497
+ // off below, since it also fires on every draft commit.
1498
+ const resetPage = resetPageOnQueryChange ?? true;
1499
+ const getQueryTable = useCallback(
1500
+ () => tableRef.current as unknown as TMDataGridQueryTable,
1501
+ [],
1502
+ );
1503
+
1504
+ // The sync of `state` into the table's atoms happens inside this call, in
1505
+ // the render body, and publishing from there makes React warn about the
1506
+ // consumer's component - see controlledStateSync.ts.
1507
+ beginControlledStateSync();
1172
1508
  const table = useTable({
1173
1509
  // The grid paints a running drag itself, one style write per frame - see
1174
1510
  // the resize preview in TMDataGridTable - and takes the width into state
1175
1511
  // once, when the pointer is released. `"onChange"` publishes a width on
1176
1512
  // every pointer move instead, which re-renders the grid for each of them.
1177
1513
  columnResizeMode: "onEnd",
1514
+ // TanStack resets both whenever the `data` array's identity changes, and
1515
+ // the rows as shown are a new array on every draft commit (see `shown`),
1516
+ // so a commit on page 3 would land on page 1 with every details panel
1517
+ // closed. Off here; the page reset the grid does want - on a query
1518
+ // change - is its own, below. A consumer's explicit option still wins.
1519
+ autoResetExpanded: false,
1520
+ autoResetPageIndex: false,
1178
1521
  enableSorting: true,
1179
1522
  enableColumnResizing: true,
1180
1523
  // The quick search's matcher. Fuzzy by default (Q4); `"contains"` keeps
@@ -1191,6 +1534,41 @@ export function useTMDataGrid<TData extends RowData>({
1191
1534
  // `"reorder"` to keep the column and have it moved to the front instead.
1192
1535
  groupedColumnMode: "remove",
1193
1536
  ...options,
1537
+ // The query slices, wrapped so a change also takes the grid back to the
1538
+ // first page - see pageReset.ts. Spread conditionally: the keys carry a
1539
+ // `makeStateUpdater` default, and an explicit `undefined` would overwrite
1540
+ // it and leave the slice unwritable.
1541
+ ...(resetPage
1542
+ ? {
1543
+ onColumnFiltersChange: withPageReset(
1544
+ "columnFilters",
1545
+ options.onColumnFiltersChange as never,
1546
+ getQueryTable,
1547
+ ) as TableOptions<
1548
+ TMDataGridFeatures,
1549
+ TData
1550
+ >["onColumnFiltersChange"],
1551
+ onGlobalFilterChange: withPageReset(
1552
+ "globalFilter",
1553
+ options.onGlobalFilterChange as never,
1554
+ getQueryTable,
1555
+ ) as TableOptions<TMDataGridFeatures, TData>["onGlobalFilterChange"],
1556
+ onSortingChange: withPageReset(
1557
+ "sorting",
1558
+ options.onSortingChange as never,
1559
+ getQueryTable,
1560
+ ) as TableOptions<TMDataGridFeatures, TData>["onSortingChange"],
1561
+ onGroupingChange: withPageReset(
1562
+ "grouping",
1563
+ options.onGroupingChange as never,
1564
+ getQueryTable,
1565
+ ) as TableOptions<TMDataGridFeatures, TData>["onGroupingChange"],
1566
+ }
1567
+ : {}),
1568
+ // The rows as shown - see `shown` above. The consumer's own array passes
1569
+ // through untouched while nothing is committed.
1570
+ data: shown.rows,
1571
+ getRowId: shownGetRowId,
1194
1572
  // Row details ride on `expanded`, the same state the tree uses - but a data
1195
1573
  // row answers `getCanExpand()` false, since TanStack's fallback is
1196
1574
  // `subRows.length > 0`. `() => true` is the right answer for a group row
@@ -1209,6 +1587,20 @@ export function useTMDataGrid<TData extends RowData>({
1209
1587
  (typeof options.enableRowPinning === "function"
1210
1588
  ? options.enableRowPinning(row)
1211
1589
  : options.enableRowPinning === true),
1590
+ // A deletion-marked row is not selectable: it is on its way out, and a
1591
+ // bulk action over the selection must not see it. TanStack reads the
1592
+ // predicate on every call, so the mark is checked live against the
1593
+ // engine. Only under `editing.draft`, the one place marks exist; the
1594
+ // consumer's own option keeps the final say, predicate form included.
1595
+ ...(editDraft
1596
+ ? {
1597
+ enableRowSelection: (row: Row<TMDataGridFeatures, TData>) =>
1598
+ !engine.isRowDeleted(row.id) &&
1599
+ (typeof options.enableRowSelection === "function"
1600
+ ? options.enableRowSelection(row)
1601
+ : options.enableRowSelection !== false),
1602
+ }
1603
+ : {}),
1212
1604
  features: tmDataGridFeatures,
1213
1605
  columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
1214
1606
  // The stabilized controlled state; `undefined` when nothing is controlled.
@@ -1230,14 +1622,14 @@ export function useTMDataGrid<TData extends RowData>({
1230
1622
  columnPinning: {
1231
1623
  // The generated columns are structurally pinned, so they are re-applied
1232
1624
  // on top of anything restored from storage.
1233
- left: [
1625
+ start: [
1234
1626
  ...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
1235
1627
  ...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
1236
1628
  ...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
1237
1629
  ...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
1238
1630
  ...(
1239
- persistedState.columnPinning?.left ??
1240
- options.initialState?.columnPinning?.left ??
1631
+ persistedState.columnPinning?.start ??
1632
+ options.initialState?.columnPinning?.start ??
1241
1633
  []
1242
1634
  ).filter(
1243
1635
  (id) =>
@@ -1249,10 +1641,10 @@ export function useTMDataGrid<TData extends RowData>({
1249
1641
  ],
1250
1642
  // The edit lane mirrors the generated columns on the left: structurally
1251
1643
  // pinned, outermost, re-applied over anything restored.
1252
- right: [
1644
+ end: [
1253
1645
  ...(
1254
- persistedState.columnPinning?.right ??
1255
- options.initialState?.columnPinning?.right ??
1646
+ persistedState.columnPinning?.end ??
1647
+ options.initialState?.columnPinning?.end ??
1256
1648
  []
1257
1649
  ).filter((id) => id !== EDIT_COLUMN_ID),
1258
1650
  ...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
@@ -1266,12 +1658,11 @@ export function useTMDataGrid<TData extends RowData>({
1266
1658
  },
1267
1659
  },
1268
1660
  }, selectSettledState);
1661
+ endControlledStateSync();
1662
+ tableRef.current = table as unknown as TMDataGridTable<TData>;
1663
+ deferControlledStateSyncPublishes(table.store);
1269
1664
 
1270
- // The edit engine. Built once per mount; everything it needs later - the
1271
- // table, the mode, the consumer's callbacks - is read through a ref updated
1272
- // every render, so forms created at `begin()` always call the latest
1273
- // `onEditCommit` (the onHighlightedRowChangeRef pattern, applied wholesale).
1274
- const editContextRef = useRef<TMDataGridEditEngineContext>(null as never);
1665
+ // The engine's view of this render - see the engine's creation above.
1275
1666
  editContextRef.current = {
1276
1667
  table: table as unknown as TMDataGridTable<TMDataGridRowData>,
1277
1668
  editMode: editMode ?? "cell",
@@ -1284,23 +1675,14 @@ export function useTMDataGrid<TData extends RowData>({
1284
1675
  editing?.isRowEditable as TMDataGridEditEngineContext["isRowEditable"],
1285
1676
  onEditCommit:
1286
1677
  editing?.onCommit as TMDataGridEditEngineContext["onEditCommit"],
1287
- // The deprecated name still works; the new one wins if both are set.
1288
- onSaveDrafts: (editing?.onSaveDrafts ??
1289
- editing?.onCommitDrafts) as TMDataGridEditEngineContext["onSaveDrafts"],
1678
+ onSaveDrafts:
1679
+ editing?.onSaveDrafts as TMDataGridEditEngineContext["onSaveDrafts"],
1290
1680
  newRowDefaults:
1291
1681
  editing?.newRowDefaults as TMDataGridEditEngineContext["newRowDefaults"],
1292
1682
  onRowAdd: editing?.onRowAdd as TMDataGridEditEngineContext["onRowAdd"],
1293
1683
  onRowDelete:
1294
1684
  editing?.onRowDelete as TMDataGridEditEngineContext["onRowDelete"],
1295
1685
  };
1296
- // The engine is erased; the row type comes back on the way out, which is
1297
- // what makes `edit.addRow(values)` check against `TData`.
1298
- const [edit] = useState(
1299
- () =>
1300
- createEditEngine(
1301
- () => editContextRef.current,
1302
- ) as unknown as TMDataGridEditApi<TData>,
1303
- );
1304
1686
 
1305
1687
  // Switching either axis mid-flight drops every draft: the policies disagree
1306
1688
  // about what an open form means, and carrying one across is how a row
@@ -1315,6 +1697,29 @@ export function useTMDataGrid<TData extends RowData>({
1315
1697
  edit.cancelAll();
1316
1698
  }, [editMode, editDraft, edit]);
1317
1699
 
1700
+ // A refetch that drops a record takes the engine's state for it along -
1701
+ // see forgetMissingRows. Not where the grid does not own the result set:
1702
+ // there a row missing from `data` is on another page or filtered away
1703
+ // server-side, not gone, and its draft has to wait for Save. Keyed on the
1704
+ // rows as shown, which is what the core row model is built from.
1705
+ // Through the ref, not `table`: `useTable` hands out a fresh copy of the
1706
+ // table every render, and listing it would run this on every one.
1707
+ const editingOn = editing !== undefined;
1708
+ const serverSideRows =
1709
+ options.manualPagination === true || options.manualFiltering === true;
1710
+ useEffect(() => {
1711
+ if (!editingOn || serverSideRows) return;
1712
+ engine.forgetMissingRows(tableRef.current.getCoreRowModel().rowsById);
1713
+ }, [shown.rows, editingOn, serverSideRows, engine]);
1714
+
1715
+ // Readers that hold the table and nothing else - `exportGrid` - still
1716
+ // leave a deletion-marked row out. Keyed on
1717
+ // the store, which every copy of the table shares, so once is enough.
1718
+ useEffect(
1719
+ () => registerDeletedRows(tableRef.current, engine.isRowDeleted),
1720
+ [engine],
1721
+ );
1722
+
1318
1723
  // Editing without stable ids points every draft at whatever record slides
1319
1724
  // into that index after a sort. Loud, once, in development.
1320
1725
  useEffect(() => {
@@ -1341,42 +1746,16 @@ export function useTMDataGrid<TData extends RowData>({
1341
1746
  // eslint-disable-next-line react-hooks/exhaustive-deps
1342
1747
  }, []);
1343
1748
 
1344
- // Two things have to happen whenever `grouping` changes.
1345
- //
1346
- // One: the tree column appears with the first grouped column and goes away
1347
- // with the last, so an ungrouped grid looks exactly as it did before grouping
1749
+ // The tree column appears with the first grouped column and goes away with
1750
+ // the last, so an ungrouped grid looks exactly as it did before grouping
1348
1751
  // existed. Driven from a subscription rather than by rebuilding the column
1349
1752
  // array, because the array is what the table is built from - deriving it from
1350
1753
  // table state would close the loop. Visibility is the one column property
1351
1754
  // that can be changed after the fact without touching the definitions.
1352
1755
  //
1353
- // Two, and this one is a workaround. In table-core 9.0.0-beta.21 the
1354
- // per-region column APIs do not list `grouping` among their memo
1355
- // dependencies, even though they all derive from `getAllLeafColumns()`, which
1356
- // does:
1357
- //
1358
- // | API | Declares |
1359
- // | --- | --- |
1360
- // | `getLeft/Center/RightVisibleLeafColumns` | columns, columnPinning, columnVisibility, columnOrder |
1361
- // | `getLeft/Center/RightHeaderGroups` | columnPinning, columnOrder |
1362
- // | `row.getLeft/Center/RightVisibleCells` | columnPinning, columnVisibility |
1363
- //
1364
- // So grouping a *second* column leaves every one of them returning the
1365
- // previous list: the column TanStack removed keeps its header and its grid
1366
- // track, and the row cells no longer line up with them. The first grouping
1367
- // appears to work only because the visibility write above happens to touch a
1368
- // dependency they share.
1369
- //
1370
- // Re-publishing `columnVisibility` and `columnOrder` - same contents, new
1371
- // identity - invalidates all three families. `columnOrder` is the only
1372
- // dependency the header groups declare, and `columnVisibility` the only one
1373
- // the cells do, so both are needed. Remove this once the deps are fixed
1374
- // upstream; the test that fails without it groups two columns and asserts the
1375
- // second one leaves the grid.
1376
- //
1377
1756
  // Writing back into the store from its own subscriber is safe: the guard is
1378
- // on `grouping`'s identity, and neither write touches it, so the callback
1379
- // these writes trigger short-circuits.
1757
+ // on `grouping`'s identity, and the write does not touch it, so the callback
1758
+ // it triggers short-circuits.
1380
1759
  useEffect(() => {
1381
1760
  if (!groupColumnEnabled) return;
1382
1761
  let previousGrouping = table.store.state.grouping;
@@ -1410,8 +1789,8 @@ export function useTMDataGrid<TData extends RowData>({
1410
1789
  // Keeps the entry injected into a controlled `columnVisibility` (see
1411
1790
  // requestedState) in sync with grouping.
1412
1791
  groupingActiveRef.current = state.grouping.length > 0;
1413
- // On a controlled slice the writes below round-trip through the
1414
- // consumer's handler; the next render must forward them unstabilized.
1792
+ // On a controlled slice the write below round-trips through the
1793
+ // consumer's handler; the next render must forward it unstabilized.
1415
1794
  // See republishControlledStateRef.
1416
1795
  republishControlledStateRef.current = true;
1417
1796
 
@@ -1419,7 +1798,6 @@ export function useTMDataGrid<TData extends RowData>({
1419
1798
  ...old,
1420
1799
  [GROUP_COLUMN_ID]: state.grouping.length > 0,
1421
1800
  }));
1422
- table.setColumnOrder((old) => [...old]);
1423
1801
  });
1424
1802
 
1425
1803
  return () => subscription.unsubscribe();
@@ -1464,9 +1842,11 @@ export function useTMDataGrid<TData extends RowData>({
1464
1842
 
1465
1843
  const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
1466
1844
  {
1467
- filterPanelOpen: false,
1468
- columnsPanelOpen: false,
1845
+ // `useCreateStore` builds the store once per mount, so `defaultOpen` is
1846
+ // read the way `initialState` is - a starting point, not a controller.
1847
+ filterPanelOpen: filters.defaultOpen,
1469
1848
  filterPanelColumnId: null,
1849
+ headerFilterColumnId: null,
1470
1850
  draggedColumnId: null,
1471
1851
  // `useCreateStore` builds the store once per mount, so this is a genuine
1472
1852
  // default rather than a value that would fight later clicks.
@@ -1474,6 +1854,7 @@ export function useTMDataGrid<TData extends RowData>({
1474
1854
  selectionAnchorRowId: null,
1475
1855
  focusedCell: null,
1476
1856
  cellRange: null,
1857
+ exportPicker: null,
1477
1858
  },
1478
1859
  ({ setState }) => ({
1479
1860
  openFilterPanel: (columnId = null) =>
@@ -1488,10 +1869,14 @@ export function useTMDataGrid<TData extends RowData>({
1488
1869
  filterPanelOpen: false,
1489
1870
  filterPanelColumnId: null,
1490
1871
  })),
1491
- setColumnsPanelOpen: (open) =>
1492
- setState((prev) => ({ ...prev, columnsPanelOpen: open })),
1493
- toggleColumnsPanel: () =>
1494
- setState((prev) => ({ ...prev, columnsPanelOpen: !prev.columnsPanelOpen })),
1872
+ openExportPicker: (request) =>
1873
+ setState((prev) => ({ ...prev, exportPicker: request })),
1874
+ closeExportPicker: () =>
1875
+ setState((prev) => ({ ...prev, exportPicker: null })),
1876
+ focusPanelFilter: (columnId) =>
1877
+ setState((prev) => ({ ...prev, filterPanelColumnId: columnId })),
1878
+ focusHeaderFilter: (columnId) =>
1879
+ setState((prev) => ({ ...prev, headerFilterColumnId: columnId })),
1495
1880
  startColumnDrag: (columnId) =>
1496
1881
  setState((prev) => ({ ...prev, draggedColumnId: columnId })),
1497
1882
  endColumnDrag: () =>
@@ -1580,12 +1965,12 @@ export function useTMDataGrid<TData extends RowData>({
1580
1965
  table.setColumnSizing({ ...initial?.columnSizing });
1581
1966
  table.setColumnOrder([...(initial?.columnOrder ?? [])]);
1582
1967
  table.setColumnPinning({
1583
- left: [
1968
+ start: [
1584
1969
  ...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
1585
1970
  ...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
1586
1971
  ...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
1587
1972
  ...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
1588
- ...(initial?.columnPinning?.left ?? []).filter(
1973
+ ...(initial?.columnPinning?.start ?? []).filter(
1589
1974
  (id) =>
1590
1975
  id !== ROW_NUMBER_COLUMN_ID &&
1591
1976
  id !== SELECT_COLUMN_ID &&
@@ -1593,8 +1978,8 @@ export function useTMDataGrid<TData extends RowData>({
1593
1978
  id !== GROUP_COLUMN_ID,
1594
1979
  ),
1595
1980
  ],
1596
- right: [
1597
- ...(initial?.columnPinning?.right ?? []).filter(
1981
+ end: [
1982
+ ...(initial?.columnPinning?.end ?? []).filter(
1598
1983
  (id) => id !== EDIT_COLUMN_ID,
1599
1984
  ),
1600
1985
  ...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
@@ -1617,6 +2002,8 @@ export function useTMDataGrid<TData extends RowData>({
1617
2002
  ui,
1618
2003
  edit,
1619
2004
  features,
2005
+ filters,
2006
+ exportOptions,
1620
2007
  labels,
1621
2008
  renderDetails,
1622
2009
  renderDetailsEstHeight,
@@ -1628,20 +2015,44 @@ export function useTMDataGrid<TData extends RowData>({
1628
2015
  }
1629
2016
 
1630
2017
  /**
1631
- * Opens the filter panel for a column, seeding an empty filter row when the
1632
- * column has none yet - mirrors "Filter" in the column header menu.
2018
+ * Gives a column an empty filter of its default operator, unless it already
2019
+ * has one - which is what makes a surface open on a row rather than on
2020
+ * nothing.
2021
+ *
2022
+ * @internal Shared by `openColumnFilter` and the toolbar's filter button.
1633
2023
  */
1634
- export function openColumnFilter<TData extends RowData>(
2024
+ export function seedColumnFilter<TData extends RowData>(
1635
2025
  api: TMDataGridApi<TData>,
1636
2026
  columnId: string,
1637
2027
  ): void {
1638
2028
  const column = api.table.getColumn(columnId);
1639
- if (column && column.getFilterValue() === undefined) {
1640
- const operator = getColumnDefaultOperator(column);
1641
- column.setFilterValue({
1642
- operator,
1643
- value: emptyValueForOperator(operator),
1644
- });
2029
+ if (column === undefined || column.getFilterValue() !== undefined) return;
2030
+ const operator = getColumnDefaultOperator(column);
2031
+ column.setFilterValue({ operator, value: emptyValueForOperator(operator) });
2032
+ }
2033
+
2034
+ /**
2035
+ * Sends the user to a column's filter control, seeding an empty filter when
2036
+ * the column has none yet - what "Filter" in the column menu and a click on a
2037
+ * filter pill both do.
2038
+ *
2039
+ * Which control that is follows the grid's `filters` option. Under
2040
+ * `inHeader` it is the column's header control, which is already on screen, so
2041
+ * the call focuses it and leaves the popup or sidebar closed. Otherwise it is
2042
+ * the panel's row for that column, and the call opens the surface on it.
2043
+ */
2044
+ export function openColumnFilter<TData extends RowData>(
2045
+ api: TMDataGridApi<TData>,
2046
+ columnId: string,
2047
+ ): void {
2048
+ seedColumnFilter(api, columnId);
2049
+ if (api.filters.inHeader) {
2050
+ // The header control is always visible, so there is nothing to open -
2051
+ // only a column to point at. The header row watching this focuses it and
2052
+ // scrolls it into view. The panel, if one is also showing, is left alone:
2053
+ // it reads the other slot.
2054
+ api.ui.actions.focusHeaderFilter(columnId);
2055
+ return;
1645
2056
  }
1646
2057
  api.ui.actions.openFilterPanel(columnId);
1647
2058
  }