@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.1

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 (157) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  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 +269 -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 +49 -48
  47. package/skills/columns/SKILL.md +125 -70
  48. package/skills/columns/references/columns-api.md +59 -0
  49. package/skills/data/SKILL.md +112 -18
  50. package/skills/editing/SKILL.md +76 -42
  51. package/skills/editing/references/common-mistakes.md +77 -69
  52. package/skills/editing/references/editing-api.md +25 -20
  53. package/skills/editing/references/editors-and-validation.md +80 -18
  54. package/skills/filtering/SKILL.md +155 -41
  55. package/skills/getting-started/SKILL.md +116 -16
  56. package/skills/grouping/SKILL.md +31 -16
  57. package/skills/migrating-to-2/SKILL.md +244 -0
  58. package/skills/options/SKILL.md +24 -12
  59. package/skills/rows/SKILL.md +22 -18
  60. package/skills/rows/references/rows-api.md +10 -6
  61. package/skills/server-side/SKILL.md +170 -17
  62. package/skills/testing/SKILL.md +150 -32
  63. package/skills/testing-components/SKILL.md +230 -0
  64. package/skills/testing-editing/SKILL.md +240 -0
  65. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  66. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  68. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  70. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  71. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  72. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  73. package/src/components/TMDataGridExportPicker.module.css +77 -0
  74. package/src/components/TMDataGridExportPicker.tsx +234 -0
  75. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  76. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  78. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  79. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  80. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  81. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  83. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  84. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  85. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  86. package/src/components/TMDataGridMenu.tsx +357 -0
  87. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  89. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  90. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  91. package/src/components/TMDataGridToolbar.tsx +181 -0
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  98. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  99. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  100. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  101. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  103. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  104. package/src/components/filters/controlLayout.ts +32 -0
  105. package/src/components/filters/filterControlFor.ts +65 -0
  106. package/src/components/generatedColumns.tsx +187 -0
  107. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  108. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  109. package/src/components/useHideableColumns.ts +52 -0
  110. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  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 +1107 -460
  118. package/src/core/export.ts +704 -0
  119. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  120. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  121. package/src/core/filterSurface.ts +99 -0
  122. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  123. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  124. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  125. package/src/core/pageReset.ts +120 -0
  126. package/src/core/pagination.ts +81 -0
  127. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  128. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  129. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  130. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  131. package/src/useTMDataGridExport.ts +78 -0
  132. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  133. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  134. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  135. package/src/tmdatagrid/core/cellExport.ts +0 -320
  136. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  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}/capabilities.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  156. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  157. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -1,8 +1,12 @@
1
1
  import { useCreateStore, useSelector } from "@tanstack/react-store";
2
2
  import { shallow } from "@tanstack/store";
3
3
  import {
4
+ type AccessorFn,
5
+ type AccessorFnColumnDef,
6
+ type AccessorKeyColumnDef,
4
7
  aggregationFns,
5
8
  type ColumnDef,
9
+ type ColumnHelper,
6
10
  columnFacetingFeature,
7
11
  columnFilteringFeature,
8
12
  columnGroupingFeature,
@@ -19,11 +23,17 @@ import {
19
23
  createFilteredRowModel,
20
24
  createGroupedRowModel,
21
25
  createPaginatedRowModel,
26
+ type DeepKeys,
27
+ type DeepValue,
28
+ type DisplayColumnDef,
22
29
  filterFns,
30
+ type GroupColumnDef,
23
31
  globalFilteringFeature,
32
+ type IdentifiedColumnDef,
24
33
  metaHelper,
25
34
  type Row,
26
35
  type RowData,
36
+ rowAggregationFeature,
27
37
  rowExpandingFeature,
28
38
  rowPaginationFeature,
29
39
  rowPinningFeature,
@@ -53,7 +63,7 @@ import {
53
63
  type TMDataGridEditApi,
54
64
  type TMDataGridEditCommitArgs,
55
65
  type TMDataGridSaveDraftsArgs,
56
- type TMDataGridSaveDraftsResult,
66
+ type TMDataGridSaveDraftsResponse,
57
67
  type TMDataGridEditEngineContext,
58
68
  type TMDataGridColumnEditOptions,
59
69
  type TMDataGridEditMode,
@@ -69,6 +79,11 @@ import {
69
79
  } from "./core/filterOperators";
70
80
  import { getColumnDefaultOperator, isControlColumn } from "./core/columnUtils";
71
81
  import type { TMDataGridColumnFilterOptions } from "./core/filterControls";
82
+ import {
83
+ resolveFilterOptions,
84
+ type TMDataGridFiltersOptions,
85
+ type TMDataGridFiltersSettings,
86
+ } from "./core/filterSurface";
72
87
  import {
73
88
  createFuzzyRankedSortedRowModel,
74
89
  fuzzyGlobalFilterFn,
@@ -101,23 +116,34 @@ import {
101
116
  stabilizeControlledState,
102
117
  withoutUndefinedSlices,
103
118
  } from "./core/controlledState";
104
- import type { TMDataGridCellRange } from "./core/cellRange";
105
119
  import {
106
- createSelectColumn,
107
- SELECT_COLUMN_ID,
108
- } from "./components/TMDataGridSelectColumn";
120
+ beginControlledStateSync,
121
+ deferControlledStateSyncPublishes,
122
+ endControlledStateSync,
123
+ } from "./core/controlledStateSync";
124
+ import { registerDeletedRows } from "./core/deletedRows";
109
125
  import {
110
- createGroupColumn,
111
- GROUP_COLUMN_ID,
112
- } from "./components/TMDataGridGroupColumn";
126
+ withPageReset,
127
+ type TMDataGridQueryTable,
128
+ } from "./core/pageReset";
129
+ import type { TMDataGridCellRange } from "./core/cellRange";
113
130
  import {
114
- createDetailsColumn,
115
- DETAILS_COLUMN_ID,
116
- } 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";
117
141
  import {
142
+ createDetailsColumn,
118
143
  createEditColumn,
119
- EDIT_COLUMN_ID,
120
- } from "./components/TMDataGridEditColumn";
144
+ createGroupColumn,
145
+ createSelectColumn,
146
+ } from "./components/generatedColumns";
121
147
  import {
122
148
  createRowNumberColumn,
123
149
  ROW_NUMBER_COLUMN_ID,
@@ -139,8 +165,11 @@ const PERSIST_DEBOUNCE_MS = 200;
139
165
  * `type` and `options` are read by both stages, so one declaration of each
140
166
  * feeds the filter panel and the cell editor, which is why they sit outside
141
167
  * both namespaces.
168
+ *
169
+ * `TData` types the row that `options` and `edit.enabled` callbacks receive.
170
+ * `createTMDataGridColumnHelper<TData>()` fills it in.
142
171
  */
143
- export type TMDataGridColumnMeta = {
172
+ export type TMDataGridColumnMeta<TData extends RowData = TMDataGridRowData> = {
144
173
  /** Name shown in menus and the column manager. Falls back to a string header. */
145
174
  label?: string;
146
175
  /**
@@ -155,7 +184,7 @@ export type TMDataGridColumnMeta = {
155
184
  * function of the table, column and, for editors, the row. See
156
185
  * {@link TMDataGridOptionsSource}.
157
186
  */
158
- options?: TMDataGridOptionsSource;
187
+ options?: TMDataGridOptionsSource<TData>;
159
188
  /** Share of the leftover width this column claims. Defaults to `1`. */
160
189
  flex?: number;
161
190
  align?: "left" | "right" | "center";
@@ -172,8 +201,8 @@ export type TMDataGridColumnMeta = {
172
201
  */
173
202
  enableOrdering?: boolean;
174
203
  /**
175
- * How this column filters: the operator a fresh filter starts with, and the
176
- * 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.
177
206
  *
178
207
  * ```tsx
179
208
  * meta: {
@@ -199,7 +228,26 @@ export type TMDataGridColumnMeta = {
199
228
  *
200
229
  * See {@link TMDataGridColumnEditOptions}.
201
230
  */
202
- 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;
203
251
  };
204
252
 
205
253
  /** Grid-wide configuration passed through `options.meta`. */
@@ -232,6 +280,7 @@ export const tmDataGridFeatures = tableFeatures({
232
280
  columnResizingFeature,
233
281
  columnFacetingFeature,
234
282
  columnGroupingFeature,
283
+ rowAggregationFeature,
235
284
  // Registered for grouping's sake rather than for tree data: the grouped row
236
285
  // model builds the parent rows, and this is what flattens the expanded ones
237
286
  // back into the flat list the body virtualizes.
@@ -271,8 +320,57 @@ export type TMDataGridTable<TData extends RowData> = Table<
271
320
  TData
272
321
  >;
273
322
 
274
- export function createTMDataGridColumnHelper<TData extends RowData>() {
275
- 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>;
276
374
  }
277
375
 
278
376
  /** What the `renderDetails` render prop is handed for an expanded row. */
@@ -305,9 +403,20 @@ const EMPTY_IDS: ReadonlyArray<string> = [];
305
403
  */
306
404
  export type TMDataGridUiState = {
307
405
  filterPanelOpen: boolean;
308
- columnsPanelOpen: boolean;
309
- /** 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
+ */
310
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;
311
420
  /**
312
421
  * Column being dragged by its header, if any. Held here rather than read from
313
422
  * `dataTransfer`, which browsers keep unreadable until the drop.
@@ -346,13 +455,33 @@ export type TMDataGridUiState = {
346
455
  * describe different places.
347
456
  */
348
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;
349
465
  };
350
466
 
351
467
  export type TMDataGridUiActions = {
352
468
  openFilterPanel: (columnId?: string | null) => void;
353
469
  closeFilterPanel: () => void;
354
- setColumnsPanelOpen: (open: boolean) => void;
355
- 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;
356
485
  startColumnDrag: (columnId: string) => void;
357
486
  endColumnDrag: () => void;
358
487
  /**
@@ -397,13 +526,28 @@ export type TMDataGridApi<TData extends RowData> = {
397
526
  ui: TMDataGridUiStore;
398
527
  /**
399
528
  * The edit engine - open forms, dirty/error projections, and the verbs
400
- * (`begin`, `commit`, `cancel`, `submitAll`). `edit.getForm(rowId)` hands
401
- * out the same TanStack Form the inline editors write through, so a drawer
402
- * 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.
403
534
  */
404
535
  edit: TMDataGridEditApi<TData>;
405
536
  /** Table-level feature switches, re-read from options on every render. */
406
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;
407
551
  /** Every string the chrome renders, `labels` merged over the English defaults. */
408
552
  labels: TMDataGridLabels;
409
553
  /** The detail renderer, when row details are on. See `renderDetails`. */
@@ -592,25 +736,14 @@ export type TMDataGridEditingOptions<TData extends RowData> =
592
736
  *
593
737
  * Rows still open are not in the payload and stay open. Returning
594
738
  * nothing saves the whole store and throwing saves none of it;
595
- * return a {@link TMDataGridSaveDraftsResult} to save part of it.
739
+ * return a {@link TMDataGridSaveDraftsResponse} to save part of it.
596
740
  */
597
741
  onSaveDrafts?: (
598
742
  args: TMDataGridSaveDraftsArgs<TData>,
599
743
  ) =>
600
744
  | void
601
- | TMDataGridSaveDraftsResult
602
- | Promise<void | TMDataGridSaveDraftsResult>;
603
- /**
604
- * @deprecated Renamed to {@link onSaveDrafts} - it fires when the
605
- * draft store is saved, not when a row commits into it. Still
606
- * honoured; removed in a later beta.
607
- */
608
- onCommitDrafts?: (
609
- args: TMDataGridSaveDraftsArgs<TData>,
610
- ) =>
611
- | void
612
- | TMDataGridSaveDraftsResult
613
- | Promise<void | TMDataGridSaveDraftsResult>;
745
+ | TMDataGridSaveDraftsResponse
746
+ | Promise<void | TMDataGridSaveDraftsResponse>;
614
747
  /**
615
748
  * Keep committed entry rows pinned in the sticky entry block until
616
749
  * the draft store is saved, out of the body's sort. Off by default:
@@ -626,8 +759,6 @@ export type TMDataGridEditingOptions<TData extends RowData> =
626
759
  draft?: false;
627
760
  /** Only `draft: true` has a store to save - see the other branch. */
628
761
  onSaveDrafts?: never;
629
- /** @deprecated See {@link onSaveDrafts}. */
630
- onCommitDrafts?: never;
631
762
  /** Parked entry rows exist only under `draft: true` - see there. */
632
763
  newRowsSticky?: never;
633
764
  }
@@ -693,12 +824,55 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
693
824
  * no extra flag.
694
825
  */
695
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;
696
840
  /**
697
841
  * The row-number gutter: a generated lane, outermost left, numbering the
698
842
  * rows of the current view - sorted, filtered, continuing across pages,
699
843
  * with group rows unnumbered. Off by default.
700
844
  */
701
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;
702
876
  /**
703
877
  * How the quick search (`TMDataGrid.Search`) matches. `"fuzzy"` - the
704
878
  * default - forgives typos and skipped characters, and while it is the
@@ -962,6 +1136,7 @@ export function useTMDataGrid<TData extends RowData>({
962
1136
  labels: labelsOverride,
963
1137
  enableColumnOrdering,
964
1138
  enablePagination,
1139
+ resetPageOnQueryChange,
965
1140
  enableRowNumbers,
966
1141
  selectionMode,
967
1142
  showSelectedBackground,
@@ -969,6 +1144,8 @@ export function useTMDataGrid<TData extends RowData>({
969
1144
  onHighlightedRowChange,
970
1145
  cellSelection,
971
1146
  onFocusedCellChange,
1147
+ filters: filterOptions,
1148
+ exportOptions: exportOptionsOverride,
972
1149
  editing,
973
1150
  renderDetails,
974
1151
  renderDetailsEstHeight = DEFAULT_DETAILS_EST_HEIGHT,
@@ -1000,6 +1177,50 @@ export function useTMDataGrid<TData extends RowData>({
1000
1177
  // Resolved on the override's identity, so a module-scope dictionary costs one
1001
1178
  // merge for the lifetime of the grid.
1002
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
+ );
1003
1224
 
1004
1225
  const pinningEnabled = options.enableColumnPinning !== false;
1005
1226
  const selectColumnEnabled = features.selectColumn;
@@ -1184,12 +1405,10 @@ export function useTMDataGrid<TData extends RowData>({
1184
1405
  const editContextRef = useRef<TMDataGridEditEngineContext>(null as never);
1185
1406
  // The engine is erased; the row type comes back on the way out, which is
1186
1407
  // what makes `edit.addRow(values)` check against `TData`.
1187
- const [edit] = useState(
1188
- () =>
1189
- createEditEngine(
1190
- () => editContextRef.current,
1191
- ) as unknown as TMDataGridEditApi<TData>,
1408
+ const [engine] = useState(() =>
1409
+ createEditEngine(() => editContextRef.current),
1192
1410
  );
1411
+ const edit = engine as unknown as TMDataGridEditApi<TData>;
1193
1412
 
1194
1413
  // The rows as shown. Under `editing.draft` a committed row is a row like any
1195
1414
  // other to the table: its draft replaces the consumer's record and a
@@ -1269,12 +1488,36 @@ export function useTMDataGrid<TData extends RowData>({
1269
1488
  [],
1270
1489
  );
1271
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();
1272
1508
  const table = useTable({
1273
1509
  // The grid paints a running drag itself, one style write per frame - see
1274
1510
  // the resize preview in TMDataGridTable - and takes the width into state
1275
1511
  // once, when the pointer is released. `"onChange"` publishes a width on
1276
1512
  // every pointer move instead, which re-renders the grid for each of them.
1277
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,
1278
1521
  enableSorting: true,
1279
1522
  enableColumnResizing: true,
1280
1523
  // The quick search's matcher. Fuzzy by default (Q4); `"contains"` keeps
@@ -1291,6 +1534,37 @@ export function useTMDataGrid<TData extends RowData>({
1291
1534
  // `"reorder"` to keep the column and have it moved to the front instead.
1292
1535
  groupedColumnMode: "remove",
1293
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
+ : {}),
1294
1568
  // The rows as shown - see `shown` above. The consumer's own array passes
1295
1569
  // through untouched while nothing is committed.
1296
1570
  data: shown.rows,
@@ -1313,6 +1587,20 @@ export function useTMDataGrid<TData extends RowData>({
1313
1587
  (typeof options.enableRowPinning === "function"
1314
1588
  ? options.enableRowPinning(row)
1315
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
+ : {}),
1316
1604
  features: tmDataGridFeatures,
1317
1605
  columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
1318
1606
  // The stabilized controlled state; `undefined` when nothing is controlled.
@@ -1334,14 +1622,14 @@ export function useTMDataGrid<TData extends RowData>({
1334
1622
  columnPinning: {
1335
1623
  // The generated columns are structurally pinned, so they are re-applied
1336
1624
  // on top of anything restored from storage.
1337
- left: [
1625
+ start: [
1338
1626
  ...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
1339
1627
  ...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
1340
1628
  ...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
1341
1629
  ...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
1342
1630
  ...(
1343
- persistedState.columnPinning?.left ??
1344
- options.initialState?.columnPinning?.left ??
1631
+ persistedState.columnPinning?.start ??
1632
+ options.initialState?.columnPinning?.start ??
1345
1633
  []
1346
1634
  ).filter(
1347
1635
  (id) =>
@@ -1353,10 +1641,10 @@ export function useTMDataGrid<TData extends RowData>({
1353
1641
  ],
1354
1642
  // The edit lane mirrors the generated columns on the left: structurally
1355
1643
  // pinned, outermost, re-applied over anything restored.
1356
- right: [
1644
+ end: [
1357
1645
  ...(
1358
- persistedState.columnPinning?.right ??
1359
- options.initialState?.columnPinning?.right ??
1646
+ persistedState.columnPinning?.end ??
1647
+ options.initialState?.columnPinning?.end ??
1360
1648
  []
1361
1649
  ).filter((id) => id !== EDIT_COLUMN_ID),
1362
1650
  ...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
@@ -1370,6 +1658,9 @@ export function useTMDataGrid<TData extends RowData>({
1370
1658
  },
1371
1659
  },
1372
1660
  }, selectSettledState);
1661
+ endControlledStateSync();
1662
+ tableRef.current = table as unknown as TMDataGridTable<TData>;
1663
+ deferControlledStateSyncPublishes(table.store);
1373
1664
 
1374
1665
  // The engine's view of this render - see the engine's creation above.
1375
1666
  editContextRef.current = {
@@ -1384,9 +1675,8 @@ export function useTMDataGrid<TData extends RowData>({
1384
1675
  editing?.isRowEditable as TMDataGridEditEngineContext["isRowEditable"],
1385
1676
  onEditCommit:
1386
1677
  editing?.onCommit as TMDataGridEditEngineContext["onEditCommit"],
1387
- // The deprecated name still works; the new one wins if both are set.
1388
- onSaveDrafts: (editing?.onSaveDrafts ??
1389
- editing?.onCommitDrafts) as TMDataGridEditEngineContext["onSaveDrafts"],
1678
+ onSaveDrafts:
1679
+ editing?.onSaveDrafts as TMDataGridEditEngineContext["onSaveDrafts"],
1390
1680
  newRowDefaults:
1391
1681
  editing?.newRowDefaults as TMDataGridEditEngineContext["newRowDefaults"],
1392
1682
  onRowAdd: editing?.onRowAdd as TMDataGridEditEngineContext["onRowAdd"],
@@ -1407,6 +1697,29 @@ export function useTMDataGrid<TData extends RowData>({
1407
1697
  edit.cancelAll();
1408
1698
  }, [editMode, editDraft, edit]);
1409
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
+
1410
1723
  // Editing without stable ids points every draft at whatever record slides
1411
1724
  // into that index after a sort. Loud, once, in development.
1412
1725
  useEffect(() => {
@@ -1433,42 +1746,16 @@ export function useTMDataGrid<TData extends RowData>({
1433
1746
  // eslint-disable-next-line react-hooks/exhaustive-deps
1434
1747
  }, []);
1435
1748
 
1436
- // Two things have to happen whenever `grouping` changes.
1437
- //
1438
- // One: the tree column appears with the first grouped column and goes away
1439
- // 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
1440
1751
  // existed. Driven from a subscription rather than by rebuilding the column
1441
1752
  // array, because the array is what the table is built from - deriving it from
1442
1753
  // table state would close the loop. Visibility is the one column property
1443
1754
  // that can be changed after the fact without touching the definitions.
1444
1755
  //
1445
- // Two, and this one is a workaround. In table-core 9.0.0-beta.21 the
1446
- // per-region column APIs do not list `grouping` among their memo
1447
- // dependencies, even though they all derive from `getAllLeafColumns()`, which
1448
- // does:
1449
- //
1450
- // | API | Declares |
1451
- // | --- | --- |
1452
- // | `getLeft/Center/RightVisibleLeafColumns` | columns, columnPinning, columnVisibility, columnOrder |
1453
- // | `getLeft/Center/RightHeaderGroups` | columnPinning, columnOrder |
1454
- // | `row.getLeft/Center/RightVisibleCells` | columnPinning, columnVisibility |
1455
- //
1456
- // So grouping a *second* column leaves every one of them returning the
1457
- // previous list: the column TanStack removed keeps its header and its grid
1458
- // track, and the row cells no longer line up with them. The first grouping
1459
- // appears to work only because the visibility write above happens to touch a
1460
- // dependency they share.
1461
- //
1462
- // Re-publishing `columnVisibility` and `columnOrder` - same contents, new
1463
- // identity - invalidates all three families. `columnOrder` is the only
1464
- // dependency the header groups declare, and `columnVisibility` the only one
1465
- // the cells do, so both are needed. Remove this once the deps are fixed
1466
- // upstream; the test that fails without it groups two columns and asserts the
1467
- // second one leaves the grid.
1468
- //
1469
1756
  // Writing back into the store from its own subscriber is safe: the guard is
1470
- // on `grouping`'s identity, and neither write touches it, so the callback
1471
- // 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.
1472
1759
  useEffect(() => {
1473
1760
  if (!groupColumnEnabled) return;
1474
1761
  let previousGrouping = table.store.state.grouping;
@@ -1502,8 +1789,8 @@ export function useTMDataGrid<TData extends RowData>({
1502
1789
  // Keeps the entry injected into a controlled `columnVisibility` (see
1503
1790
  // requestedState) in sync with grouping.
1504
1791
  groupingActiveRef.current = state.grouping.length > 0;
1505
- // On a controlled slice the writes below round-trip through the
1506
- // 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.
1507
1794
  // See republishControlledStateRef.
1508
1795
  republishControlledStateRef.current = true;
1509
1796
 
@@ -1511,7 +1798,6 @@ export function useTMDataGrid<TData extends RowData>({
1511
1798
  ...old,
1512
1799
  [GROUP_COLUMN_ID]: state.grouping.length > 0,
1513
1800
  }));
1514
- table.setColumnOrder((old) => [...old]);
1515
1801
  });
1516
1802
 
1517
1803
  return () => subscription.unsubscribe();
@@ -1556,9 +1842,11 @@ export function useTMDataGrid<TData extends RowData>({
1556
1842
 
1557
1843
  const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
1558
1844
  {
1559
- filterPanelOpen: false,
1560
- 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,
1561
1848
  filterPanelColumnId: null,
1849
+ headerFilterColumnId: null,
1562
1850
  draggedColumnId: null,
1563
1851
  // `useCreateStore` builds the store once per mount, so this is a genuine
1564
1852
  // default rather than a value that would fight later clicks.
@@ -1566,6 +1854,7 @@ export function useTMDataGrid<TData extends RowData>({
1566
1854
  selectionAnchorRowId: null,
1567
1855
  focusedCell: null,
1568
1856
  cellRange: null,
1857
+ exportPicker: null,
1569
1858
  },
1570
1859
  ({ setState }) => ({
1571
1860
  openFilterPanel: (columnId = null) =>
@@ -1580,10 +1869,14 @@ export function useTMDataGrid<TData extends RowData>({
1580
1869
  filterPanelOpen: false,
1581
1870
  filterPanelColumnId: null,
1582
1871
  })),
1583
- setColumnsPanelOpen: (open) =>
1584
- setState((prev) => ({ ...prev, columnsPanelOpen: open })),
1585
- toggleColumnsPanel: () =>
1586
- 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 })),
1587
1880
  startColumnDrag: (columnId) =>
1588
1881
  setState((prev) => ({ ...prev, draggedColumnId: columnId })),
1589
1882
  endColumnDrag: () =>
@@ -1672,12 +1965,12 @@ export function useTMDataGrid<TData extends RowData>({
1672
1965
  table.setColumnSizing({ ...initial?.columnSizing });
1673
1966
  table.setColumnOrder([...(initial?.columnOrder ?? [])]);
1674
1967
  table.setColumnPinning({
1675
- left: [
1968
+ start: [
1676
1969
  ...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
1677
1970
  ...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
1678
1971
  ...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
1679
1972
  ...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
1680
- ...(initial?.columnPinning?.left ?? []).filter(
1973
+ ...(initial?.columnPinning?.start ?? []).filter(
1681
1974
  (id) =>
1682
1975
  id !== ROW_NUMBER_COLUMN_ID &&
1683
1976
  id !== SELECT_COLUMN_ID &&
@@ -1685,8 +1978,8 @@ export function useTMDataGrid<TData extends RowData>({
1685
1978
  id !== GROUP_COLUMN_ID,
1686
1979
  ),
1687
1980
  ],
1688
- right: [
1689
- ...(initial?.columnPinning?.right ?? []).filter(
1981
+ end: [
1982
+ ...(initial?.columnPinning?.end ?? []).filter(
1690
1983
  (id) => id !== EDIT_COLUMN_ID,
1691
1984
  ),
1692
1985
  ...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
@@ -1709,6 +2002,8 @@ export function useTMDataGrid<TData extends RowData>({
1709
2002
  ui,
1710
2003
  edit,
1711
2004
  features,
2005
+ filters,
2006
+ exportOptions,
1712
2007
  labels,
1713
2008
  renderDetails,
1714
2009
  renderDetailsEstHeight,
@@ -1720,20 +2015,44 @@ export function useTMDataGrid<TData extends RowData>({
1720
2015
  }
1721
2016
 
1722
2017
  /**
1723
- * Opens the filter panel for a column, seeding an empty filter row when the
1724
- * 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.
1725
2023
  */
1726
- export function openColumnFilter<TData extends RowData>(
2024
+ export function seedColumnFilter<TData extends RowData>(
1727
2025
  api: TMDataGridApi<TData>,
1728
2026
  columnId: string,
1729
2027
  ): void {
1730
2028
  const column = api.table.getColumn(columnId);
1731
- if (column && column.getFilterValue() === undefined) {
1732
- const operator = getColumnDefaultOperator(column);
1733
- column.setFilterValue({
1734
- operator,
1735
- value: emptyValueForOperator(operator),
1736
- });
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;
1737
2056
  }
1738
2057
  api.ui.actions.openFilterPanel(columnId);
1739
2058
  }