@jielga/tmdatagrid 2.0.0-beta.9 → 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 (154) 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 +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 +46 -47
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +67 -40
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +22 -19
  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 +7 -7
  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/components → components}/TMDataGrid.tsx +38 -21
  63. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  64. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  65. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  66. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  67. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  68. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  69. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  70. package/src/components/TMDataGridExportPicker.module.css +77 -0
  71. package/src/components/TMDataGridExportPicker.tsx +234 -0
  72. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  73. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  74. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  75. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  76. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  78. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  79. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  80. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  81. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  82. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  83. package/src/components/TMDataGridMenu.tsx +357 -0
  84. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  85. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  86. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  87. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  88. package/src/components/TMDataGridToolbar.tsx +181 -0
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  96. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  97. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  98. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  99. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  100. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  101. package/src/components/filters/controlLayout.ts +32 -0
  102. package/src/components/filters/filterControlFor.ts +65 -0
  103. package/src/components/generatedColumns.tsx +187 -0
  104. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  105. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  106. package/src/components/useHideableColumns.ts +52 -0
  107. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  108. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  109. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  110. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  111. package/src/core/controlledStateSync.ts +108 -0
  112. package/src/core/deletedRows.ts +34 -0
  113. package/src/core/dom.ts +74 -0
  114. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  115. package/src/core/export.ts +704 -0
  116. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  117. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  118. package/src/core/filterSurface.ts +99 -0
  119. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  120. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  121. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  122. package/src/core/pageReset.ts +120 -0
  123. package/src/core/pagination.ts +81 -0
  124. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  125. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  126. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  127. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  128. package/src/useTMDataGridExport.ts +78 -0
  129. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  130. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  131. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  134. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  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}/capabilities.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/useSettledTableState.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
+ }
@@ -19,3 +19,24 @@ export function getGroupDataRows<TData extends RowData>(
19
19
  if (row.subRows.length === 0) return [row];
20
20
  return row.getLeafRows().filter((leaf) => leaf.subRows.length === 0);
21
21
  }
22
+
23
+ /** Shown for a group whose value is empty - `String(null)` would read as "null". */
24
+ const BLANK_GROUP_LABEL = "(Blank)";
25
+
26
+ /**
27
+ * How a grouping value is written into the tree cell.
28
+ *
29
+ * Deliberately not the grouped column's own `cell` renderer: that renderer is
30
+ * written for a data row and is free to reach into `row.original`, which on a
31
+ * group row is the first leaf's record rather than anything about the group.
32
+ */
33
+ export function formatGroupValue(
34
+ value: unknown,
35
+ blankLabel = BLANK_GROUP_LABEL,
36
+ ): string {
37
+ if (value === null || value === undefined || value === "") {
38
+ return blankLabel;
39
+ }
40
+ if (value instanceof Date) return value.toLocaleDateString();
41
+ return String(value);
42
+ }
@@ -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;
@@ -140,9 +148,25 @@ export type TMDataGridLabels = {
140
148
  // Cell selection menu
141
149
  cellCount: (count: number) => string;
142
150
  copy: string;
143
- exportCsv: string;
151
+ exportCells: string;
144
152
  includeHeaders: string;
145
153
 
154
+ // Export
155
+ exportAll: string;
156
+ exportSelected: (count: number) => string;
157
+ /** Title of the column picker `columns="custom"` opens; `format` is the file extension in upper case, "CSV". */
158
+ exportPickerTitle: (format: string) => string;
159
+ /** The line under the title; `selected` is the selected row count, or `null` for an export of all rows. */
160
+ exportPickerHint: (selected: number | null) => string;
161
+ exportPickerConfirm: string;
162
+ exportPickerCancel: string;
163
+ /** The select-all row over the picker's list. */
164
+ exportPickerSelectAll: string;
165
+ /** The "3 of 12" count beside it. */
166
+ exportPickerCount: (checked: number, total: number) => string;
167
+ /** Marks a column the grid hides at the moment. */
168
+ exportPickerHidden: string;
169
+
146
170
  // Generated checkbox lane
147
171
  selectColumnLabel: string;
148
172
  selectAllRows: string;
@@ -177,16 +201,18 @@ export type TMDataGridLabelsOverride = Partial<
177
201
  export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
178
202
  operators: FILTER_OPERATOR_LABELS,
179
203
 
180
- manageColumns: "Manage columns",
181
204
  filters: "Filters",
182
205
  searchPlaceholder: "Search",
183
206
  searchLabel: "Search rows",
184
207
  clearSearch: "Clear search",
185
208
 
209
+ menuButton: "Menu",
210
+ manageColumns: "Manage columns",
211
+
186
212
  columnsSearchPlaceholder: "Search",
187
213
  columnsNoMatch: (search) => `No columns match “${search}”`,
188
214
  columnsShowHideAll: "Show/Hide All",
189
- columnsReset: "RESET LAYOUT",
215
+ columnsReset: "Reset layout",
190
216
  columnsResetHint:
191
217
  "Resets column visibility, order, widths, pinning and grouping",
192
218
  rowNumberColumnLabel: "Row number",
@@ -205,6 +231,7 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
205
231
  clearAllFilters: "Clear all",
206
232
  closeFilters: "Close filters",
207
233
  removeFilter: "Remove filter",
234
+ filterOperatorFor: (column) => `${column} filter operator`,
208
235
 
209
236
  activeFilters: "Active filters",
210
237
  clearFilter: (column) => `Clear ${column} filter`,
@@ -229,6 +256,8 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
229
256
 
230
257
  rowsPerPage: "Rows per page:",
231
258
  pageRange: ({ from, to, total }) => `${from}–${to} of ${total}`,
259
+ pageNumber: ({ page, pageCount }) =>
260
+ pageCount < 0 ? `Page ${page}` : `Page ${page} of ${pageCount}`,
232
261
  groupedAllRows: (total) => `Grouped · all ${total} rows`,
233
262
  pagingSuspendedHint:
234
263
  "Paging is off while the rows are grouped: the whole tree is rendered and virtualized. Ungroup to page again.",
@@ -261,9 +290,25 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
261
290
 
262
291
  cellCount: (count) => (count === 1 ? "1 cell" : `${count} cells`),
263
292
  copy: "Copy",
264
- exportCsv: "Export as CSV for Excel",
293
+ exportCells: "Export cells",
265
294
  includeHeaders: "Include headers",
266
295
 
296
+ exportAll: "Export all rows",
297
+ exportSelected: (count) =>
298
+ count === 1 ? "Export 1 selected row" : `Export ${count} selected rows`,
299
+ exportPickerTitle: (format) => `Export as ${format}`,
300
+ exportPickerHint: (selected) =>
301
+ selected === null
302
+ ? "Select the columns to export"
303
+ : selected === 1
304
+ ? "Select the columns to export for the selected row"
305
+ : `Select the columns to export for the ${selected} selected rows`,
306
+ exportPickerConfirm: "Export",
307
+ exportPickerCancel: "Cancel",
308
+ exportPickerSelectAll: "Select all",
309
+ exportPickerCount: (checked, total) => `${checked} of ${total}`,
310
+ exportPickerHidden: "Hidden",
311
+
267
312
  selectColumnLabel: "Checkbox selection",
268
313
  selectAllRows: "Select all rows",
269
314
  selectRow: "Select row",
@@ -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.",
@@ -116,13 +121,29 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
116
121
 
117
122
  cellCount: (count) => (count === 1 ? "1 cell" : `${count} celler`),
118
123
  copy: "Kopiera",
119
- exportCsv: "Exportera som CSV för Excel",
124
+ exportCells: "Exportera celler",
120
125
  includeHeaders: "Inkludera rubriker",
121
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: "Välj alla",
140
+ exportPickerCount: (checked, total) => `${checked} av ${total}`,
141
+ exportPickerHidden: "Dold",
142
+
122
143
  selectColumnLabel: "Kryssrutemarkering",
123
- selectAllRows: "Markera alla rader",
124
- selectRow: "Markera rad",
125
- selectGroup: "Markera grupp",
144
+ selectAllRows: "Välj alla rader",
145
+ selectRow: "Välj rad",
146
+ selectGroup: "Välj grupp",
126
147
 
127
148
  detailsColumnLabel: "Detaljer",
128
149
  showDetails: "Visa detaljer",
@@ -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
+ }
@@ -0,0 +1,81 @@
1
+ import type { RowData } from "@tanstack/react-table";
2
+ import type { TMDataGridTable } from "../useTMDataGrid";
3
+
4
+ /** What the pager is showing. The read half of {@link TMDataGridPaginationApi}. */
5
+ export type TMDataGridPaginationState = {
6
+ pageIndex: number;
7
+ pageSize: number;
8
+ /** `-1` when a manual grid declares `pageCount: -1` (unknown total). */
9
+ pageCount: number;
10
+ rowCount: number;
11
+ canPreviousPage: boolean;
12
+ canNextPage: boolean;
13
+ /**
14
+ * Whether the pager is slicing anything right now. `false` while a grouping
15
+ * is active, which suspends paging - see `isPagingActive`.
16
+ */
17
+ isPagingActive: boolean;
18
+ /** First and last row number on this page, 1-based, for a range label. */
19
+ from: number;
20
+ to: number;
21
+ };
22
+
23
+ /** What the pager can do. The write half of {@link TMDataGridPaginationApi}. */
24
+ export type TMDataGridPaginationActions = {
25
+ setPageIndex: (pageIndex: number) => void;
26
+ setPageSize: (pageSize: number) => void;
27
+ previousPage: () => void;
28
+ nextPage: () => void;
29
+ firstPage: () => void;
30
+ lastPage: () => void;
31
+ };
32
+
33
+ /**
34
+ * Pagination state and actions, split into the half you read and the half you
35
+ * call.
36
+ *
37
+ * The split is what makes a partial override possible: a consumer replacing
38
+ * only the range label reads `state` and never touches `actions`, and one
39
+ * replacing only the buttons does the opposite.
40
+ */
41
+ export type TMDataGridPaginationApi = {
42
+ state: TMDataGridPaginationState;
43
+ actions: TMDataGridPaginationActions;
44
+ };
45
+
46
+ /**
47
+ * Reads {@link TMDataGridPaginationApi} off a table. The Footer feeds it to the
48
+ * `renderPagination` slot; a pager living outside the Footer can call it with
49
+ * the table from `useTMDataGrid` directly.
50
+ *
51
+ * `Controls` are not here: they are components bound to the grid context, and
52
+ * this function takes only a table.
53
+ */
54
+ export function getTMDataGridPaginationApi<TData extends RowData>(
55
+ table: TMDataGridTable<TData>,
56
+ isPaging = true,
57
+ ): TMDataGridPaginationApi {
58
+ const { pageIndex, pageSize } = table.store.state.pagination;
59
+ const rowCount = table.getRowCount();
60
+ return {
61
+ state: {
62
+ pageIndex,
63
+ pageSize,
64
+ pageCount: table.getPageCount(),
65
+ rowCount,
66
+ canPreviousPage: table.getCanPreviousPage(),
67
+ canNextPage: table.getCanNextPage(),
68
+ isPagingActive: isPaging,
69
+ from: rowCount === 0 ? 0 : pageIndex * pageSize + 1,
70
+ to: Math.min(rowCount, (pageIndex + 1) * pageSize),
71
+ },
72
+ actions: {
73
+ setPageIndex: (index) => table.setPageIndex(index),
74
+ setPageSize: (size) => table.setPageSize(size),
75
+ previousPage: () => table.previousPage(),
76
+ nextPage: () => table.nextPage(),
77
+ firstPage: () => table.firstPage(),
78
+ lastPage: () => table.lastPage(),
79
+ },
80
+ };
81
+ }