@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -34,8 +34,36 @@ export type TMDataGridFilterControlArgs = {
34
34
  options: ReadonlyArray<TMDataGridOption>;
35
35
  size: TMDataGridSize;
36
36
  labels: TMDataGridLabels;
37
+ /**
38
+ * How much room the control has, and whether it names itself. The same
39
+ * vocabulary as `TMDataGrid.FilterPanel`'s own `layout` prop, plus the one
40
+ * value only a header cell can be in.
41
+ *
42
+ * | Layout | Where | Field |
43
+ * | --- | --- | --- |
44
+ * | `"row"` | A filter row laid out side by side | Labelled, fixed width |
45
+ * | `"stacked"` | A filter row in a narrow host - the sidebar | Labelled, full width |
46
+ * | `"header"` | One header cell, under `filters.inHeader` | `aria-label`, full width |
47
+ *
48
+ * Every built-in control honours it. A custom control that ignores it still
49
+ * works - it will simply look the same everywhere.
50
+ */
51
+ layout: TMDataGridFilterControlLayout;
37
52
  };
38
53
 
54
+ /** How much room a filter control has. See `layout`. */
55
+ export type TMDataGridFilterControlLayout = "row" | "stacked" | "header";
56
+
57
+ /**
58
+ * The two a filter *panel* can be in - {@link TMDataGridFilterControlLayout}
59
+ * without the header cell, which is not a panel. `TMDataGrid.FilterPanel`'s
60
+ * `layout` prop.
61
+ */
62
+ export type TMDataGridFilterPanelLayout = Exclude<
63
+ TMDataGridFilterControlLayout,
64
+ "header"
65
+ >;
66
+
39
67
  /**
40
68
  * `meta.filter.control` - replaces the built-in value control for this column.
41
69
  * Rendered as JSX, never invoked as a bare function, so hooks are legal
@@ -58,10 +86,19 @@ export type TMDataGridFilterControlComponent =
58
86
  * declaration of each feeds the filter panel and the cell editor alike.
59
87
  */
60
88
  export type TMDataGridColumnFilterOptions = {
89
+ /**
90
+ * The operators this column offers, a subset of the type's own. For a
91
+ * column backed by an endpoint that answers only some of them - `contains`
92
+ * and `equals`, say - so the panel and the header funnel never offer an
93
+ * operator the query cannot express. Kept in the type's order; one the type
94
+ * does not offer is ignored, and a list that leaves nothing falls back to
95
+ * the type's full set.
96
+ */
97
+ operators?: readonly TMDataGridFilterOperator[];
61
98
  /**
62
99
  * The operator a fresh filter on this column starts with, instead of the
63
100
  * type's default - a salary column can open on `"between"`. Must be one of
64
- * the type's own operators.
101
+ * the operators the column offers.
65
102
  */
66
103
  defaultOperator?: TMDataGridFilterOperator;
67
104
  /**
@@ -1,4 +1,10 @@
1
- import type { Row, RowData, TableFeatures } from "@tanstack/react-table";
1
+ import type {
2
+ ColumnFiltersState,
3
+ Row,
4
+ RowData,
5
+ TableFeatures,
6
+ } from "@tanstack/react-table";
7
+ import type { TMDataGridTable } from "../useTMDataGrid";
2
8
 
3
9
  /**
4
10
  * The value shape stored in `columnFilters` for every TMDataGrid column.
@@ -210,6 +216,25 @@ export function isTMDataGridFilterValue(
210
216
  );
211
217
  }
212
218
 
219
+ /**
220
+ * The three value shapes an operator can take. A set is not a range, even
221
+ * though both are arrays.
222
+ */
223
+ export type TMDataGridFilterValueShape = "scalar" | "set" | "range";
224
+
225
+ /**
226
+ * Which shape an operator's value takes. A typed value survives an operator or
227
+ * column change only within its shape, which is the rule both the panel and
228
+ * the header controls use when the operator changes.
229
+ */
230
+ export function filterValueShape(
231
+ operator: TMDataGridFilterOperator,
232
+ ): TMDataGridFilterValueShape {
233
+ if (operatorTakesArrayValue(operator)) return "set";
234
+ if (operatorTakesRangeValue(operator)) return "range";
235
+ return "scalar";
236
+ }
237
+
213
238
  /**
214
239
  * A filter only narrows the row set once it has something to compare against.
215
240
  * Half-typed filters stay in state (so the panel keeps rendering their row) but
@@ -225,6 +250,44 @@ export function isFilterActive(value: unknown): boolean {
225
250
  : typeof value.value === "string" && value.value.trim() !== "";
226
251
  }
227
252
 
253
+ /** One column's filter, typed - what `columnFilters` holds per entry. */
254
+ export type TMDataGridColumnFilter = {
255
+ id: string;
256
+ value: TMDataGridFilterValue;
257
+ };
258
+
259
+ /**
260
+ * The column filters that are actually narrowing the grid, typed.
261
+ *
262
+ * `ColumnFiltersState` types `value` as `unknown`, so the first line of a
263
+ * server-side mapping layer is otherwise a cast back to the shape the grid
264
+ * itself wrote, wrapped in the same "drop the half-typed ones" filter every
265
+ * consumer writes:
266
+ *
267
+ * ```ts
268
+ * const predicates = activeColumnFilters(table).map((filter) =>
269
+ * toPredicate(filter.id, filter.value),
270
+ * );
271
+ * ```
272
+ *
273
+ * Takes the table, or a `columnFilters` array where the consumer owns the
274
+ * slice. Reading it from the table reads the current value and does not
275
+ * subscribe; inside a component, subscribe to `columnFilters` the way the
276
+ * grid's own chrome does.
277
+ */
278
+ export function activeColumnFilters<TData extends RowData>(
279
+ source: TMDataGridTable<TData> | ColumnFiltersState,
280
+ ): Array<TMDataGridColumnFilter> {
281
+ const columnFilters = Array.isArray(source)
282
+ ? source
283
+ : source.store.state.columnFilters;
284
+ return columnFilters.flatMap((entry) =>
285
+ isTMDataGridFilterValue(entry.value) && isFilterActive(entry.value)
286
+ ? [{ id: entry.id, value: entry.value }]
287
+ : [],
288
+ );
289
+ }
290
+
228
291
  /**
229
292
  * One-line description of a single filter, as shown on a filter pill.
230
293
  *
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Where the grid puts its filter controls.
3
+ *
4
+ * | Surface | Where it renders |
5
+ * | --- | --- |
6
+ * | `"popup"` | Floating over the first body rows, under the header |
7
+ * | `"sidebar"` | Beside the table, inside the grid frame |
8
+ * | `"none"` | Nowhere - the grid renders no panel of its own |
9
+ *
10
+ * Header filters are not one of these: they are a second row of controls in
11
+ * the header, always visible, and they coexist with any of the three. See
12
+ * {@link TMDataGridFiltersOptions.inHeader}.
13
+ */
14
+ export type TMDataGridFilterSurface = "popup" | "sidebar" | "none";
15
+
16
+ /** Which side of the table the sidebar surface renders on. */
17
+ export type TMDataGridFilterSidebarSide = "left" | "right";
18
+
19
+ /**
20
+ * `filters` on `useTMDataGrid` - everything about where the filter controls
21
+ * are, as opposed to what they do.
22
+ *
23
+ * Named for the option key, the way `editing` has `TMDataGridEditingOptions`.
24
+ * Not to be confused with `TMDataGridColumnFilterOptions`, which is one
25
+ * column's `meta.filter`.
26
+ *
27
+ * ```tsx
28
+ * useTMDataGrid({ data, columns, filters: { surface: "sidebar" } });
29
+ * ```
30
+ */
31
+ export type TMDataGridFiltersOptions = {
32
+ /**
33
+ * Which surface `TMDataGrid.Table` renders and `TMDataGrid.FilterButton`
34
+ * toggles. Defaults to `"popup"`.
35
+ *
36
+ * Under `"none"` the table renders no panel and the filter button renders
37
+ * nothing. That is what a grid running header filters alone wants, and it is
38
+ * also what frees a hand-placed `<TMDataGrid.FilterPanel />` to be the only
39
+ * panel on the page - mounted, it is always visible, so drive it off
40
+ * `ui.state.filterPanelOpen` if it belongs behind a control of your own.
41
+ */
42
+ surface?: TMDataGridFilterSurface;
43
+ /** Which side the `"sidebar"` surface sits on. Defaults to `"right"`. */
44
+ sidebarSide?: TMDataGridFilterSidebarSide;
45
+ /** Width of the `"sidebar"` surface, any CSS length. Defaults to `"280px"`. */
46
+ sidebarWidth?: string;
47
+ /**
48
+ * Whether the popup or the sidebar starts open. Read once, at mount, like
49
+ * `initialState`.
50
+ *
51
+ * Defaults to `true` under `"sidebar"` and `false` everywhere else: a
52
+ * sidebar is a layout choice, so asking for one and getting an empty strip
53
+ * until the funnel is clicked is not what it reads like, while a popup that
54
+ * greets you open is in the way.
55
+ *
56
+ * Under `"none"` it is simply the starting value of
57
+ * `ui.state.filterPanelOpen`, which a control of your own can read.
58
+ */
59
+ defaultOpen?: boolean;
60
+ /**
61
+ * A second header row holding one value control per filterable column,
62
+ * always visible. Off by default.
63
+ *
64
+ * Independent of `surface` - a grid may have header filters and a popup at
65
+ * once. What it does change is the column chrome: the header's funnel
66
+ * indicator and the column menu's "Filter" item both come off, because
67
+ * their only job was to reveal a control that is now already on screen.
68
+ *
69
+ * A header cell has room for a value and an operator button, not for the
70
+ * panel's column / operator / value triple. Everything else about a filter
71
+ * is unchanged - the same operators, the same `meta.filter.control`, the
72
+ * same `columnFilters` state.
73
+ */
74
+ inHeader?: boolean;
75
+ };
76
+
77
+ /** {@link TMDataGridFiltersOptions} with every default filled in. */
78
+ export type TMDataGridFiltersSettings = Required<TMDataGridFiltersOptions>;
79
+
80
+ /**
81
+ * Fills the defaults in. Field by field rather than by spreading, so an
82
+ * explicit `undefined` - which is what destructuring an absent option group
83
+ * hands over - reads as "not set" rather than overwriting the default with it.
84
+ *
85
+ * `defaultOpen` is the one default that is not a constant: it follows the
86
+ * surface, so there is no flat table of defaults to export.
87
+ */
88
+ export function resolveFilterOptions(
89
+ options: TMDataGridFiltersOptions = {},
90
+ ): TMDataGridFiltersSettings {
91
+ const surface = options.surface ?? "popup";
92
+ return {
93
+ surface,
94
+ sidebarSide: options.sidebarSide ?? "right",
95
+ sidebarWidth: options.sidebarWidth ?? "280px",
96
+ defaultOpen: options.defaultOpen ?? surface === "sidebar",
97
+ inHeader: options.inHeader ?? false,
98
+ };
99
+ }
@@ -17,14 +17,18 @@ export type TMDataGridLabels = {
17
17
  operators: Record<TMDataGridFilterOperator, string>;
18
18
 
19
19
  // Toolbar
20
- /** "Manage columns" - the burger button and the header menu item. */
21
- manageColumns: string;
22
20
  /** "Filters" - the funnel button and the filter panel's title. */
23
21
  filters: string;
24
22
  searchPlaceholder: string;
25
23
  searchLabel: string;
26
24
  clearSearch: string;
27
25
 
26
+ // Grid menu
27
+ /** Tooltip and `aria-label` of `TMDataGrid.Menu`'s burger trigger. */
28
+ menuButton: string;
29
+ /** "Manage columns" - the header menu's submenu. */
30
+ manageColumns: string;
31
+
28
32
  // Columns panel
29
33
  columnsSearchPlaceholder: string;
30
34
  columnsNoMatch: (search: string) => string;
@@ -58,6 +62,8 @@ export type TMDataGridLabels = {
58
62
  clearAllFilters: string;
59
63
  closeFilters: string;
60
64
  removeFilter: string;
65
+ /** Names the operator button in a column's header filter control. */
66
+ filterOperatorFor: (column: string) => string;
61
67
 
62
68
  // Filter pills
63
69
  activeFilters: string;
@@ -85,6 +91,8 @@ export type TMDataGridLabels = {
85
91
  // Footer / pager
86
92
  rowsPerPage: string;
87
93
  pageRange: (args: { from: number; to: number; total: number }) => string;
94
+ /** `pageCount` is `-1` when a manual grid declares an unknown total. */
95
+ pageNumber: (args: { page: number; pageCount: number }) => string;
88
96
  groupedAllRows: (total: number) => string;
89
97
  pagingSuspendedHint: string;
90
98
  previousPage: string;
@@ -111,10 +119,15 @@ export type TMDataGridLabels = {
111
119
  cancelRowEdit: string;
112
120
  /** Save tooltip when field errors block the row, with no row message. */
113
121
  editRowErrors: string;
114
- /** `EditActions`' Save, with the dirty-row count. */
122
+ /** `DraftActions`' Save, with the count of rows in the draft store. */
115
123
  saveAllEdits: (rows: number) => string;
116
- /** `EditActions`' Discard. */
124
+ /** `DraftActions`' Discard. */
117
125
  discardAllEdits: string;
126
+ /**
127
+ * `DraftActions`' note about rows still open - edited but not committed, so
128
+ * not part of the save. Shown only while there are any.
129
+ */
130
+ editRowsStillOpen: (rows: number) => string;
118
131
  /** The entry row's ✓ - commit the add. */
119
132
  confirmNewRow: string;
120
133
  /** The entry row's ✕ - drop the entry. */
@@ -135,9 +148,27 @@ export type TMDataGridLabels = {
135
148
  // Cell selection menu
136
149
  cellCount: (count: number) => string;
137
150
  copy: string;
138
- exportCsv: string;
151
+ exportCells: string;
152
+ /** @deprecated Use `exportCells`. Read as its fallback for one beta. */
153
+ exportCsv?: string;
139
154
  includeHeaders: string;
140
155
 
156
+ // Export
157
+ exportAll: string;
158
+ exportSelected: (count: number) => string;
159
+ /** Title of the column picker `columns="custom"` opens; `format` is the file extension in upper case, "CSV". */
160
+ exportPickerTitle: (format: string) => string;
161
+ /** The line under the title; `selected` is the selected row count, or `null` for an export of all rows. */
162
+ exportPickerHint: (selected: number | null) => string;
163
+ exportPickerConfirm: string;
164
+ exportPickerCancel: string;
165
+ /** The select-all row over the picker's list. */
166
+ exportPickerSelectAll: string;
167
+ /** The "3 of 12" count beside it. */
168
+ exportPickerCount: (checked: number, total: number) => string;
169
+ /** Marks a column the grid hides at the moment. */
170
+ exportPickerHidden: string;
171
+
141
172
  // Generated checkbox lane
142
173
  selectColumnLabel: string;
143
174
  selectAllRows: string;
@@ -172,16 +203,18 @@ export type TMDataGridLabelsOverride = Partial<
172
203
  export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
173
204
  operators: FILTER_OPERATOR_LABELS,
174
205
 
175
- manageColumns: "Manage columns",
176
206
  filters: "Filters",
177
207
  searchPlaceholder: "Search",
178
208
  searchLabel: "Search rows",
179
209
  clearSearch: "Clear search",
180
210
 
211
+ menuButton: "Menu",
212
+ manageColumns: "Manage columns",
213
+
181
214
  columnsSearchPlaceholder: "Search",
182
215
  columnsNoMatch: (search) => `No columns match “${search}”`,
183
216
  columnsShowHideAll: "Show/Hide All",
184
- columnsReset: "RESET LAYOUT",
217
+ columnsReset: "Reset layout",
185
218
  columnsResetHint:
186
219
  "Resets column visibility, order, widths, pinning and grouping",
187
220
  rowNumberColumnLabel: "Row number",
@@ -200,6 +233,7 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
200
233
  clearAllFilters: "Clear all",
201
234
  closeFilters: "Close filters",
202
235
  removeFilter: "Remove filter",
236
+ filterOperatorFor: (column) => `${column} filter operator`,
203
237
 
204
238
  activeFilters: "Active filters",
205
239
  clearFilter: (column) => `Clear ${column} filter`,
@@ -224,6 +258,8 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
224
258
 
225
259
  rowsPerPage: "Rows per page:",
226
260
  pageRange: ({ from, to, total }) => `${from}–${to} of ${total}`,
261
+ pageNumber: ({ page, pageCount }) =>
262
+ pageCount < 0 ? `Page ${page}` : `Page ${page} of ${pageCount}`,
227
263
  groupedAllRows: (total) => `Grouped · all ${total} rows`,
228
264
  pagingSuspendedHint:
229
265
  "Paging is off while the rows are grouped: the whole tree is rendered and virtualized. Ungroup to page again.",
@@ -243,6 +279,8 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
243
279
  editRowErrors: "Fix the marked cells",
244
280
  saveAllEdits: (rows) => (rows === 1 ? "Save 1 row" : `Save ${rows} rows`),
245
281
  discardAllEdits: "Discard",
282
+ editRowsStillOpen: (rows) =>
283
+ rows === 1 ? "1 row still being edited" : `${rows} rows still being edited`,
246
284
  confirmNewRow: "Add row",
247
285
  discardNewRow: "Discard new row",
248
286
  deleteRow: "Delete row",
@@ -254,9 +292,25 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
254
292
 
255
293
  cellCount: (count) => (count === 1 ? "1 cell" : `${count} cells`),
256
294
  copy: "Copy",
257
- exportCsv: "Export as CSV for Excel",
295
+ exportCells: "Export cells",
258
296
  includeHeaders: "Include headers",
259
297
 
298
+ exportAll: "Export all rows",
299
+ exportSelected: (count) =>
300
+ count === 1 ? "Export 1 selected row" : `Export ${count} selected rows`,
301
+ exportPickerTitle: (format) => `Export as ${format}`,
302
+ exportPickerHint: (selected) =>
303
+ selected === null
304
+ ? "Select the columns to export"
305
+ : selected === 1
306
+ ? "Select the columns to export for the selected row"
307
+ : `Select the columns to export for the ${selected} selected rows`,
308
+ exportPickerConfirm: "Export",
309
+ exportPickerCancel: "Cancel",
310
+ exportPickerSelectAll: "Select all",
311
+ exportPickerCount: (checked, total) => `${checked} of ${total}`,
312
+ exportPickerHidden: "Hidden",
313
+
260
314
  selectColumnLabel: "Checkbox selection",
261
315
  selectAllRows: "Select all rows",
262
316
  selectRow: "Select row",
@@ -282,6 +336,10 @@ export function mergeLabels(
282
336
  return {
283
337
  ...TMDATAGRID_LABELS_EN,
284
338
  ...override,
339
+ exportCells:
340
+ override.exportCells ??
341
+ override.exportCsv ??
342
+ TMDATAGRID_LABELS_EN.exportCells,
285
343
  operators: {
286
344
  ...TMDATAGRID_LABELS_EN.operators,
287
345
  ...override.operators,
@@ -32,16 +32,18 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
32
32
  isNotEmpty: "är inte tom",
33
33
  },
34
34
 
35
- manageColumns: "Hantera kolumner",
36
35
  filters: "Filter",
37
36
  searchPlaceholder: "Sök",
38
37
  searchLabel: "Sök rader",
39
38
  clearSearch: "Rensa sökningen",
40
39
 
40
+ menuButton: "Meny",
41
+ manageColumns: "Hantera kolumner",
42
+
41
43
  columnsSearchPlaceholder: "Sök",
42
44
  columnsNoMatch: (search) => `Inga kolumner matchar ”${search}”`,
43
45
  columnsShowHideAll: "Visa/dölj alla",
44
- columnsReset: "ÅTERSTÄLL LAYOUT",
46
+ columnsReset: "Återställ layout",
45
47
  columnsResetHint:
46
48
  "Återställer kolumnernas synlighet, ordning, bredder, fästning och gruppering",
47
49
  rowNumberColumnLabel: "Radnummer",
@@ -60,6 +62,7 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
60
62
  clearAllFilters: "Rensa alla",
61
63
  closeFilters: "Stäng filter",
62
64
  removeFilter: "Ta bort filter",
65
+ filterOperatorFor: (column) => `Filteroperator för ${column}`,
63
66
 
64
67
  activeFilters: "Aktiva filter",
65
68
  clearFilter: (column) => `Rensa filtret för ${column}`,
@@ -84,6 +87,8 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
84
87
 
85
88
  rowsPerPage: "Rader per sida:",
86
89
  pageRange: ({ from, to, total }) => `${from}–${to} av ${total}`,
90
+ pageNumber: ({ page, pageCount }) =>
91
+ pageCount < 0 ? `Sida ${page}` : `Sida ${page} av ${pageCount}`,
87
92
  groupedAllRows: (total) => `Grupperad · alla ${total} rader`,
88
93
  pagingSuspendedHint:
89
94
  "Sidindelningen är avstängd medan raderna är grupperade: hela trädet renderas och virtualiseras. Avgruppera för att bläddra igen.",
@@ -103,6 +108,8 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
103
108
  editRowErrors: "Åtgärda de markerade cellerna",
104
109
  saveAllEdits: (rows) => (rows === 1 ? "Spara 1 rad" : `Spara ${rows} rader`),
105
110
  discardAllEdits: "Förkasta",
111
+ editRowsStillOpen: (rows) =>
112
+ rows === 1 ? "1 rad redigeras fortfarande" : `${rows} rader redigeras fortfarande`,
106
113
  confirmNewRow: "Lägg till rad",
107
114
  discardNewRow: "Släng ny rad",
108
115
  deleteRow: "Ta bort rad",
@@ -114,9 +121,25 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
114
121
 
115
122
  cellCount: (count) => (count === 1 ? "1 cell" : `${count} celler`),
116
123
  copy: "Kopiera",
117
- exportCsv: "Exportera som CSV för Excel",
124
+ exportCells: "Exportera celler",
118
125
  includeHeaders: "Inkludera rubriker",
119
126
 
127
+ exportAll: "Exportera alla rader",
128
+ exportSelected: (count) =>
129
+ count === 1 ? "Exportera 1 vald rad" : `Exportera ${count} valda rader`,
130
+ exportPickerTitle: (format) => `Exportera som ${format}`,
131
+ exportPickerHint: (selected) =>
132
+ selected === null
133
+ ? "Välj kolumner att exportera"
134
+ : selected === 1
135
+ ? "Välj kolumner att exportera för den valda raden"
136
+ : `Välj kolumner att exportera för de ${selected} valda raderna`,
137
+ exportPickerConfirm: "Exportera",
138
+ exportPickerCancel: "Avbryt",
139
+ exportPickerSelectAll: "Markera alla",
140
+ exportPickerCount: (checked, total) => `${checked} av ${total}`,
141
+ exportPickerHidden: "Dold",
142
+
120
143
  selectColumnLabel: "Kryssrutemarkering",
121
144
  selectAllRows: "Markera alla rader",
122
145
  selectRow: "Markera rad",
@@ -0,0 +1,120 @@
1
+ import {
2
+ functionalUpdate,
3
+ makeStateUpdater,
4
+ type TableState,
5
+ type Updater,
6
+ } from "@tanstack/react-table";
7
+ import type { TMDataGridFeatures } from "../useTMDataGrid";
8
+ import { sameStateValue } from "./controlledState";
9
+ import { isFilterActive, isTMDataGridFilterValue } from "./filterOperators";
10
+
11
+ /**
12
+ * Sends a server-paged grid back to the first page when the query changes.
13
+ *
14
+ * Under `manualPagination` the grid does not own the result set, so a column
15
+ * filter, the quick search or a sort leaves `pageIndex` pointing into a page
16
+ * range that no longer exists: the next request asks the server for page 8 of
17
+ * a result set that now has three, and the consumer gets an empty grid with
18
+ * no error.
19
+ *
20
+ * TanStack's `autoResetPageIndex` does not cover it. It defaults to
21
+ * `!manualPagination`, so it is off exactly when this is needed, and it fires
22
+ * on a data change - which server-side is the response landing, one request
23
+ * too late.
24
+ *
25
+ * The reset rides on the slice's own change callback rather than on an effect
26
+ * over table state, so both writes land in the same render and the query goes
27
+ * out once. From an effect the request for the stale page would leave first.
28
+ */
29
+
30
+ /**
31
+ * The state a query is built from, plus grouping: paging is suspended while
32
+ * a column is grouped, and the page it hands back on the way out was
33
+ * measured over the ungrouped rows before the group was made.
34
+ */
35
+ export type TMDataGridQuerySlice =
36
+ | "columnFilters"
37
+ | "globalFilter"
38
+ | "sorting"
39
+ | "grouping";
40
+
41
+ /** What the reset reads and writes - the table, narrowed to that. */
42
+ export type TMDataGridQueryTable = {
43
+ store: { state: TableState<TMDataGridFeatures> };
44
+ setPageIndex: (index: number) => void;
45
+ };
46
+
47
+ type QueryHandler = (updater: never) => void;
48
+
49
+ type ColumnFilters = TableState<TMDataGridFeatures>["columnFilters"];
50
+
51
+ /**
52
+ * The entries the server could answer differently. Only the grid's own
53
+ * `{ operator, value }` shape can be recognised as "still empty" - anything
54
+ * else, a custom filter control's raw value or state restored from a URL,
55
+ * narrows the grid as far as this module can know, so it always counts.
56
+ */
57
+ function querySignificantFilters(filters: ColumnFilters): ColumnFilters {
58
+ return filters.filter(
59
+ (entry) =>
60
+ !isTMDataGridFilterValue(entry.value) || isFilterActive(entry.value),
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Whether the server would answer the next state differently.
66
+ *
67
+ * A column filter with an empty value matches every row, so it is not part of
68
+ * the query: opening the filter panel seeds a row on the first filterable
69
+ * column, and the user then types into it. Neither moves the result set, and
70
+ * neither may throw away the page the user is on.
71
+ */
72
+ function changesTheQuery(
73
+ slice: TMDataGridQuerySlice,
74
+ previous: unknown,
75
+ next: unknown,
76
+ ): boolean {
77
+ if (slice === "columnFilters") {
78
+ return !sameStateValue(
79
+ querySignificantFilters(previous as ColumnFilters),
80
+ querySignificantFilters(next as ColumnFilters),
81
+ );
82
+ }
83
+ return !sameStateValue(previous, next);
84
+ }
85
+
86
+ /**
87
+ * Wraps one slice's change callback. `handler` is the consumer's, where they
88
+ * control the slice; without one the write is the same `makeStateUpdater`
89
+ * TanStack would have defaulted to, which this option replaces.
90
+ */
91
+ export function withPageReset(
92
+ slice: TMDataGridQuerySlice,
93
+ handler: QueryHandler | undefined,
94
+ getTable: () => TMDataGridQueryTable,
95
+ ): QueryHandler {
96
+ return (updater) => {
97
+ const table = getTable();
98
+ const previous = table.store.state[slice];
99
+ // Resolved here rather than read back off the table afterwards: where the
100
+ // consumer owns the slice the write is their `setState`, so the table
101
+ // still holds the old value when this returns. The updater therefore runs
102
+ // twice per change - once here, once in the write path - which is safe
103
+ // for every updater the grid and table-core produce, and a requirement on
104
+ // the consumer's: an updater has to be a pure derivation of the previous
105
+ // value. The decision is also best-effort under batching: two writes to
106
+ // one slice in the same tick both read the same `previous`, so the second
107
+ // decision is computed from a stale base. A wrong reset decision at
108
+ // worst, never a wrong write.
109
+ const next = functionalUpdate(updater as Updater<unknown>, previous);
110
+
111
+ (handler ?? (makeStateUpdater(slice, table as never) as QueryHandler))(
112
+ updater,
113
+ );
114
+
115
+ if (!changesTheQuery(slice, previous, next)) return;
116
+ // Already on the first page: writing anyway would publish the store and
117
+ // re-render the grid for a value that did not move.
118
+ if (table.store.state.pagination.pageIndex !== 0) table.setPageIndex(0);
119
+ };
120
+ }
@@ -188,12 +188,26 @@ const SLICE_GUARDS: Record<
188
188
  isRecordOf(value, (entry) => typeof entry === "boolean"),
189
189
  columnSizing: (value) => isRecordOf(value, isFiniteNumber),
190
190
  columnOrder: (value) => isArrayOf(value, isString),
191
+ // Both the current logical shape and the physical one 1.x wrote.
191
192
  columnPinning: (value) =>
192
- isArrayOf(prop(value, "left"), isString) &&
193
- isArrayOf(prop(value, "right"), isString),
193
+ (isArrayOf(prop(value, "start"), isString) &&
194
+ isArrayOf(prop(value, "end"), isString)) ||
195
+ (isArrayOf(prop(value, "left"), isString) &&
196
+ isArrayOf(prop(value, "right"), isString)),
194
197
  grouping: (value) => isArrayOf(value, isString),
195
198
  };
196
199
 
200
+ /**
201
+ * Settings saved by 1.x used TanStack's old physical `columnPinning` keys.
202
+ * Rewrite them once on read, so nothing downstream sees `left` / `right`.
203
+ */
204
+ function normalizeColumnPinning(value: unknown): unknown {
205
+ const left = prop(value, "left");
206
+ const right = prop(value, "right");
207
+ if (left === undefined || right === undefined) return value;
208
+ return { start: left, end: right };
209
+ }
210
+
197
211
  function isValidSlice(slice: keyof GridState, value: unknown): boolean {
198
212
  const guard = SLICE_GUARDS[slice as keyof typeof SLICE_GUARDS];
199
213
  return guard === undefined || guard(value);
@@ -219,7 +233,10 @@ function readSlices(
219
233
  for (const slice of resolved.slices) {
220
234
  if (!(slice in record)) continue;
221
235
  if (!isValidSlice(slice, record[slice])) continue;
222
- restored[slice] = record[slice];
236
+ restored[slice] =
237
+ slice === "columnPinning"
238
+ ? normalizeColumnPinning(record[slice])
239
+ : record[slice];
223
240
  }
224
241
  return restored as Partial<GridState>;
225
242
  } catch {
@@ -323,8 +340,8 @@ function realignToColumns(
323
340
  }
324
341
  if (restored.columnPinning) {
325
342
  realigned.columnPinning = {
326
- left: keepIds(restored.columnPinning.left ?? []),
327
- right: keepIds(restored.columnPinning.right ?? []),
343
+ start: keepIds(restored.columnPinning.start ?? []),
344
+ end: keepIds(restored.columnPinning.end ?? []),
328
345
  };
329
346
  }
330
347
  if (restored.sorting) realigned.sorting = keepEntries(restored.sorting);