@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
@@ -14,12 +14,12 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.9'
17
+ library_version: '2.0.0'
18
18
  sources:
19
- - 'Jielga/TMDataGrid:src/docs/grouping.md'
20
- - 'Jielga/TMDataGrid:src/docs/summary-row.md'
21
- - 'Jielga/TMDataGrid:src/tmdatagrid/core/grouping.ts'
22
- - 'Jielga/TMDataGrid:src/tmdatagrid/core/summary.ts'
19
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/grouping.md'
20
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/summary-row.md'
21
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/grouping.ts'
22
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/summary.ts'
23
23
  ---
24
24
 
25
25
  # TMDataGrid - Grouping and totals
@@ -86,9 +86,12 @@ would pass a row that looks real but is the wrong one. Group rows therefore:
86
86
  - do not fire `onRowClick` or the cell handlers
87
87
  - cannot be highlighted, pinned, or given a details panel
88
88
  - never edit
89
+ - are still handed to `rowStyle` and `rowClassName`, with that child's record
90
+ as `original`, so guard a callback reading `original` with
91
+ `row.getIsGrouped()` and colour the group rows with `--dg-row-group-bg`
89
92
  - carry `data-grouped="true"` and `data-depth`, with `--dg-row-group-bg`
90
- behind them. `data-grouped` is on every row, `"true"` or `"false"`, so match
91
- the value rather than the bare attribute
93
+ behind them. `data-grouped` is present only on group rows, so
94
+ `[data-grouped]` and `[data-grouped="true"]` are equivalent
92
95
 
93
96
  A group row's checkbox selects every record under it at any depth, including
94
97
  records inside collapsed sub-groups, showing a tick once all are selected and a
@@ -142,7 +145,9 @@ aggregateColumn({ table, columnId: "location", fn: "uniqueCount" });
142
145
  ```
143
146
 
144
147
  It follows the filters deliberately. A total that does not change as the user
145
- narrows the grid is misleading.
148
+ narrows the grid is misleading. Its only argument is `table`, so a toolbar
149
+ readout or any other component holding the table reads the same total without a
150
+ `footer`.
146
151
 
147
152
  Pinned columns keep their lanes in the summary row, the generated lanes define
148
153
  no `footer` so their cells stay blank, and the row is sticky at
@@ -168,7 +173,7 @@ Correct:
168
173
  columnHelper.accessor("salary", { header: "Salary", aggregationFn: "sum" });
169
174
  ```
170
175
 
171
- Source: `src/docs/grouping.md` (Aggregation).
176
+ Source: `packages/tmdatagrid/docs/grouping.md` (Aggregation).
172
177
 
173
178
  ### HIGH Looking for the grouped column in the grid
174
179
 
@@ -183,7 +188,7 @@ Correct, when the column must stay:
183
188
  useTMDataGrid({ data, columns, groupedColumnMode: "reorder" });
184
189
  ```
185
190
 
186
- Source: `src/docs/grouping.md` (What grouping does to the grid).
191
+ Source: `packages/tmdatagrid/docs/grouping.md` (What grouping does to the grid).
187
192
 
188
193
  ### HIGH Combining the pager with grouping
189
194
 
@@ -192,14 +197,16 @@ out instead of paging the tree, so a footer count wired to `getPageCount()`
192
197
  reports a number nobody can navigate to. Read
193
198
  `isPagingActive(table, features)` before trusting the pager state.
194
199
 
195
- Source: `src/docs/grouping.md` (Grouping suspends pagination).
200
+ Source: `packages/tmdatagrid/docs/grouping.md` (Grouping suspends pagination).
196
201
 
197
202
  ### HIGH Handing a group row to a row callback
198
203
 
199
204
  Group rows do not fire `onRowClick` or the cell handlers, and cannot be pinned,
200
205
  expanded or edited. Their `row.original` is an arbitrary child's record, so a
201
206
  bulk action built from `row.original` on the tree lane acts on one record
202
- instead of the group.
207
+ instead of the group. `rowStyle` and `rowClassName` are the callbacks group
208
+ rows do reach, so one reading `row.original` colours the group by whichever
209
+ child came first.
203
210
 
204
211
  Correct:
205
212
 
@@ -207,9 +214,17 @@ Correct:
207
214
  import { getGroupDataRows } from "@jielga/tmdatagrid";
208
215
 
209
216
  const records = getGroupDataRows(groupRow).map((row) => row.original);
217
+
218
+ <TMDataGrid.Table<Employee>
219
+ rowStyle={(row) =>
220
+ !row.getIsGrouped() && row.original.status === "Terminated"
221
+ ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent)" }
222
+ : undefined
223
+ }
224
+ />;
210
225
  ```
211
226
 
212
- Source: `src/docs/grouping.md` (Group rows are not data rows).
227
+ Source: `packages/tmdatagrid/docs/grouping.md` (Group rows are not data rows).
213
228
 
214
229
  ### MEDIUM Totalling the page instead of the data
215
230
 
@@ -217,7 +232,7 @@ Source: `src/docs/grouping.md` (Group rows are not data rows).
217
232
  `table.getRowModel().rows` instead totals only what is currently paged in, and
218
233
  under virtualization not even that: only the mounted rows.
219
234
 
220
- Source: `src/docs/summary-row.md` (Totalling a column).
235
+ Source: `packages/tmdatagrid/docs/summary-row.md` (Totalling a column).
221
236
 
222
237
  ### MEDIUM Grouping a server-paged grid
223
238
 
@@ -237,7 +252,7 @@ useTMDataGrid({
237
252
  });
238
253
  ```
239
254
 
240
- Source: `src/docs/grouping.md` (Server-side grids).
255
+ Source: `packages/tmdatagrid/docs/grouping.md` (Server-side grids).
241
256
 
242
257
  ## Reference
243
258
 
@@ -258,7 +273,7 @@ Source: `src/docs/grouping.md` (Server-side grids).
258
273
  | `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything. `false` while grouped. |
259
274
  | `--dg-row-group-bg` | CSS variable | colour | Themed | Group row background. |
260
275
  | `--dg-summary-height` | CSS variable | length | From `size` | Height of the summary row. |
261
- | `data-grouped` · `data-depth` | Data attributes | – | – | `"true"` on group rows (published on every row), and the nesting level on every row. |
276
+ | `data-grouped` · `data-depth` | Data attributes | – | – | `"true"` on group rows (absent on the rest), and the nesting level on every row. |
262
277
 
263
278
  See also: the `rows` skill for selection and the details lane, and the `data`
264
279
  skill for the pager grouping suspends.
@@ -14,11 +14,11 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.9'
17
+ library_version: '2.0.0'
18
18
  sources:
19
- - 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
20
- - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
21
- - 'Jielga/TMDataGrid:src/tmdatagrid/core/persistence.ts'
19
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/use-tm-data-grid.md'
20
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/useTMDataGrid.tsx'
21
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/persistence.ts'
22
22
  ---
23
23
 
24
24
  # TMDataGrid - useTMDataGrid
@@ -74,7 +74,7 @@ rather than forwarded to TanStack.
74
74
  | Slice | Default |
75
75
  | --- | --- |
76
76
  | `pagination` | `{ pageIndex: 0, pageSize: 25 }` - inert until pagination is enabled |
77
- | `columnPinning.left` | The checkbox column, followed by any columns you provide |
77
+ | `columnPinning.start` | The checkbox column, followed by any columns you provide |
78
78
  | `globalFilterFn` | `"includesString"` |
79
79
 
80
80
  ### Controlled state
@@ -208,8 +208,8 @@ const filterPanelOpen = useSelector(grid.ui, (state) => state.filterPanelOpen);
208
208
  | --- | --- |
209
209
  | `openFilterPanel` | `(columnId?: string \| null) => void` |
210
210
  | `closeFilterPanel` | `() => void` |
211
- | `setColumnsPanelOpen` | `(open: boolean) => void` |
212
- | `toggleColumnsPanel` | `() => void` |
211
+ | `focusPanelFilter` | `(columnId: string \| null) => void` |
212
+ | `focusHeaderFilter` | `(columnId: string \| null) => void` |
213
213
  | `startColumnDrag` | `(columnId: string) => void` |
214
214
  | `endColumnDrag` | `() => void` |
215
215
  | `setHighlightedRow` | `(rowId: string \| null) => void` |
@@ -17,14 +17,15 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.9'
20
+ library_version: '2.0.0'
21
21
  sources:
22
- - 'Jielga/TMDataGrid:src/docs/row-selection.md'
23
- - 'Jielga/TMDataGrid:src/docs/row-interaction.md'
24
- - 'Jielga/TMDataGrid:src/docs/row-styling.md'
25
- - 'Jielga/TMDataGrid:src/docs/row-details.md'
26
- - 'Jielga/TMDataGrid:src/docs/row-pinning.md'
27
- - 'Jielga/TMDataGrid:src/tmdatagrid/core/rowSelection.ts'
22
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-selection.md'
23
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-interaction.md'
24
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/column-menu.md'
25
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-styling.md'
26
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-details.md'
27
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-pinning.md'
28
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/rowSelection.ts'
28
29
  ---
29
30
 
30
31
  # TMDataGrid - Rows
@@ -187,13 +188,15 @@ array rather than a node:
187
188
 
188
189
  Returning an empty list leaves the column with no menu button at all.
189
190
 
191
+ Source: `packages/tmdatagrid/docs/column-menu.md`.
192
+
190
193
  ## Row styling
191
194
 
192
195
  ```tsx
193
196
  <TMDataGrid.Table<Employee>
194
197
  striped
195
198
  rowStyle={(row) =>
196
- row.original.status === "Terminated"
199
+ !row.getIsGrouped() && row.original.status === "Terminated"
197
200
  ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent)" }
198
201
  : undefined
199
202
  }
@@ -211,8 +214,9 @@ are not striped.
211
214
  Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
212
215
  `data-grouped`, `data-depth`, `data-context-menu` and `data-row-id`, so a
213
216
  stylesheet can target any of it without a callback. The boolean attributes are
214
- published on every row as `"true"` or `"false"`, so match the value
215
- (`[data-grouped="true"]`), not the bare attribute. Pick a `--row-bg` that
217
+ present, with the value `"true"`, only while they apply - `data-grouped` only
218
+ on group rows - so `[data-grouped]` and `[data-grouped="true"]` are
219
+ equivalent. Pick a `--row-bg` that
216
220
  reads under both colour schemes - a `-0` Mantine shade is near-white and
217
221
  unreadable in dark mode; mix a mid shade into transparency instead.
218
222
 
@@ -295,7 +299,7 @@ Correct:
295
299
  rowStyle={() => ({ "--row-bg": "pink" })}
296
300
  ```
297
301
 
298
- Source: `src/docs/row-styling.md` (Set `--row-bg`, not `background`).
302
+ Source: `packages/tmdatagrid/docs/row-styling.md` (Set `--row-bg`, not `background`).
299
303
 
300
304
  ### CRITICAL Reading the selection without subscribing
301
305
 
@@ -317,7 +321,7 @@ const selected = useSelector(grid.table.store, () =>
317
321
  );
318
322
  ```
319
323
 
320
- Source: `src/docs/row-selection.md` (Acting on a selection).
324
+ Source: `packages/tmdatagrid/docs/row-selection.md` (Acting on a selection).
321
325
 
322
326
  ### HIGH Looking for the highlight in `rowSelection`
323
327
 
@@ -338,7 +342,7 @@ Correct:
338
342
  const current = useSelector(grid.ui, (state) => state.highlightedRowId);
339
343
  ```
340
344
 
341
- Source: `src/docs/row-selection.md` (The highlight is not a selection).
345
+ Source: `packages/tmdatagrid/docs/row-selection.md` (The highlight is not a selection).
342
346
 
343
347
  ### HIGH Expecting a click handler to replace the built-in behaviour
344
348
 
@@ -353,7 +357,7 @@ Correct, when the click should only navigate:
353
357
  useTMDataGrid({ data, columns, selectionMode: "highlight" });
354
358
  ```
355
359
 
356
- Source: `src/docs/row-interaction.md`.
360
+ Source: `packages/tmdatagrid/docs/row-interaction.md`.
357
361
 
358
362
  ### HIGH Reading `row.getIsExpanded()` in a cell without subscribing
359
363
 
@@ -366,7 +370,7 @@ Correct:
366
370
  const expanded = useSelector(row.table.store, () => row.getIsExpanded());
367
371
  ```
368
372
 
369
- Source: `src/docs/row-details.md` (Opening a row from elsewhere).
373
+ Source: `packages/tmdatagrid/docs/row-details.md` (Opening a row from elsewhere).
370
374
 
371
375
  ### MEDIUM Assuming group rows behave like data rows
372
376
 
@@ -375,7 +379,7 @@ Group rows are built on their first child's record. They do not fire
375
379
  never pin, they take no row number, and they have no details panel. A handler
376
380
  written as though every row reaches it silently skips them.
377
381
 
378
- Source: `src/docs/row-interaction.md`, `src/docs/row-pinning.md`.
382
+ Source: `packages/tmdatagrid/docs/row-interaction.md`, `packages/tmdatagrid/docs/row-pinning.md`.
379
383
 
380
384
  ### MEDIUM Open panels closing when `data` is replaced
381
385
 
@@ -388,7 +392,7 @@ Correct:
388
392
  useTMDataGrid({ data, columns, renderDetails, autoResetExpanded: false });
389
393
  ```
390
394
 
391
- Source: `src/docs/row-details.md`.
395
+ Source: `packages/tmdatagrid/docs/row-details.md`.
392
396
 
393
397
  ### MEDIUM Expecting pinned rows to persist
394
398
 
@@ -396,7 +400,7 @@ Source: `src/docs/row-details.md`.
396
400
  layout store outlives any one data set. A pinned id whose row leaves `data` is
397
401
  not shown, and returns to its edge if the data comes back.
398
402
 
399
- Source: `src/docs/row-pinning.md`.
403
+ Source: `packages/tmdatagrid/docs/row-pinning.md`.
400
404
 
401
405
  ## References
402
406
 
@@ -4,16 +4,22 @@ description: >
4
4
  Drive TMDataGrid from a server with TanStack manual modes - manualPagination,
5
5
  manualSorting, manualFiltering, rowCount, controlled state and onXChange
6
6
  callbacks. Covers the loading and totalRowCount meta fields, forwarding the
7
- plain-JSON columnFilters model to an API with isFilterActive, persistence
8
- interaction, and row selection across pages. Load when the grid is backed by a
9
- paginated API rather than a local array.
7
+ plain-JSON columnFilters model to an API with activeColumnFilters, mapping
8
+ filters, sorting and the page index onto an endpoint's own query language
9
+ (field table, operator table, the three value shapes, meta.filter.operators
10
+ for an endpoint that answers only some operators, keying the fetch on the
11
+ request, paging against a page envelope), the first-page reset on a query
12
+ change, persistence interaction, and row selection across pages. Load when the
13
+ grid is backed by a paginated API rather than a local array, or when
14
+ translating grid filters into server-side queries.
10
15
  metadata:
11
16
  type: core
12
17
  library: '@jielga/tmdatagrid'
13
- library_version: '2.0.0-beta.9'
18
+ library_version: '2.0.0'
14
19
  sources:
15
- - 'Jielga/TMDataGrid:src/docs/server-side.md'
16
- - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
20
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/server-side.md'
21
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/server-query.md'
22
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/useTMDataGrid.tsx'
17
23
  ---
18
24
 
19
25
  # TMDataGrid - Server-side data
@@ -68,12 +74,32 @@ const grid = useTMDataGrid({
68
74
  | --- | --- | --- |
69
75
  | Rendered rows | The current page, sliced locally | The rows returned by the server |
70
76
  | Footer total | Pre-paginated row count | `options.rowCount` |
71
- | `SummaryCount` total | Pre-filtered row count | `meta.totalRowCount` |
77
+ | `SummaryCount` total | Pre-filtered row count | `meta.totalRowCount`; the count alone without it |
72
78
  | Loading state | Not applicable | `meta.loading` |
73
79
 
74
80
  Column menus, the filter panel and the column manager behave identically in both
75
81
  modes.
76
82
 
83
+ ## The page index
84
+
85
+ Under `manualPagination` the grid resets `pageIndex` to 0 whenever the query
86
+ changes - a column filter, the quick search or the sort - so the next request
87
+ never asks for page 8 of a result set that now has three. The reset lands in
88
+ the same event as the change, so one request goes out. Pass the plain setters
89
+ as the change callbacks; do not pair them with a page reset of your own.
90
+
91
+ `resetPageOnQueryChange: false` switches it off. TanStack's
92
+ `autoResetPageIndex` is a different rule: it defaults to `!manualPagination`
93
+ and fires on a change to `data`, which server-side is the response landing.
94
+
95
+ ## Column options
96
+
97
+ `meta.options: "faceted"` reads the distinct values in `data`, which
98
+ server-side is one page of them - the dropdown then offers whatever was on the
99
+ page the user is looking at. Declare the set as a list or a function instead.
100
+ The grid warns once per column when a faceted column resolves under
101
+ `manualFiltering` or `manualPagination`.
102
+
77
103
  ## Sending filters
78
104
 
79
105
  Filter values are plain JSON, so `columnFilters` forwards without transformation:
@@ -85,17 +111,136 @@ Filter values are plain JSON, so `columnFilters` forwards without transformation
85
111
  ]
86
112
  ```
87
113
 
88
- Translate at the API boundary, and skip entries whose value is still empty -
89
- those match all rows:
114
+ Translate at the API boundary. `activeColumnFilters` hands back the entries
115
+ that narrow anything, typed - `ColumnFiltersState` types `value` as `unknown`,
116
+ and an entry whose value is still empty matches all rows:
90
117
 
91
118
  ```ts
92
- import { isFilterActive } from "@jielga/tmdatagrid";
119
+ import { activeColumnFilters } from "@jielga/tmdatagrid";
93
120
 
94
- const active = columnFilters.filter((filter) => isFilterActive(filter.value));
121
+ const active = activeColumnFilters(columnFilters);
122
+ // [{ id: "lastName", value: { operator: "contains", value: "holm" } }]
95
123
  ```
96
124
 
125
+ It takes the `columnFilters` array, or the table where the grid owns the slice.
126
+ `isFilterActive(value)` is the single-value test it is built on. Only the
127
+ grid's own `{ operator, value }` shape is read: an entry holding some other
128
+ value - a custom filter control writing raw values - is dropped.
129
+
97
130
  Debounce requests. The filter value input updates on every keystroke.
98
131
 
132
+ ## Mapping onto the endpoint's query language
133
+
134
+ An API takes a request body of its own: its own field names, its own operator
135
+ set, its own status codes, and pages counted from 1. The layer between the
136
+ grid's state and that body is one function over two lookup tables, plus one
137
+ function on the way back:
138
+
139
+ - **A field table** keyed by column id, giving the API field and the cast from
140
+ the string every filter control writes to the type the field holds
141
+ (`Number`, an enum code). A column missing from the table is one the API
142
+ cannot query: the mapping drops the filter rather than sending a field the
143
+ endpoint would reject.
144
+ - **An operator table** from `TMDataGridFilterOperator` to the API's operators.
145
+ Several grid operators collapse onto one - a date `before` and a number
146
+ `lessThan` are both `lt` once the value is cast. Declare it as a `Record`, not
147
+ a `Partial`, so an operator added by a later grid version fails the build
148
+ here rather than reaching the server unmapped.
149
+ - **`toRow`** from the API's record to the grid's row type.
150
+
151
+ ```ts
152
+ const QUERY_FIELDS: Record<string, { field: string; cast: (raw: string) => string | number }> = {
153
+ id: { field: "orderRef", cast: Number },
154
+ amount: { field: "totalAmount", cast: Number },
155
+ status: { field: "status", cast: (raw) => STATUS_CODES[raw] ?? raw },
156
+ };
157
+
158
+ const PREDICATE_OPS: Record<TMDataGridFilterOperator, PredicateOp> = {
159
+ contains: "like",
160
+ between: "range",
161
+ before: "lt",
162
+ lessThan: "lt",
163
+ isAnyOf: "in",
164
+ isEmpty: "isNull",
165
+ // ...one line for every remaining operator.
166
+ };
167
+ ```
168
+
169
+ ### The three value shapes
170
+
171
+ `TMDataGridFilterValue` is `{ operator, value }`, and the operator decides what
172
+ `value` holds. Branch on all four cases, in this order:
173
+
174
+ | Operator | `value` | Sent as |
175
+ | --- | --- | --- |
176
+ | `isEmpty`, `isNotEmpty` | Not used | `{ field, op }` |
177
+ | `isAnyOf`, `isNoneOf` | `ReadonlyArray<string>` | `{ field, op, values }` |
178
+ | `between` | `[min, max]`, either end possibly `""` | `{ field, op, from?, to? }` |
179
+ | Everything else | `string` | `{ field, op, value }` |
180
+
181
+ An empty end of a `between` pair leaves that side open: an absent bound, not an
182
+ empty string. Run `activeColumnFilters` over the slice first so a half-typed
183
+ filter is not sent as a predicate that narrows the result to nothing.
184
+
185
+ ### An endpoint that answers only some operators
186
+
187
+ Most endpoints do not have every operator the grid has - `like` and `eq` but no
188
+ prefix match is common. Do not offer what you would have to drop.
189
+ `meta.filter.operators` narrows the column to the operators the query can
190
+ express, and the mapping table is declared over exactly that list, so one
191
+ cannot be offered without a mapping or mapped without being offered:
192
+
193
+ ```ts
194
+ const TEXT_OPERATORS = [
195
+ "contains",
196
+ "equals",
197
+ "isEmpty",
198
+ "isNotEmpty",
199
+ ] as const satisfies readonly TMDataGridFilterOperator[];
200
+
201
+ const TEXT_OPS: Record<(typeof TEXT_OPERATORS)[number], PredicateOp> = {
202
+ contains: "like",
203
+ equals: "eq",
204
+ isEmpty: "isNull",
205
+ isNotEmpty: "isNotNull",
206
+ };
207
+
208
+ columnHelper.accessor("customer", {
209
+ header: "Customer",
210
+ meta: { filter: { operators: TEXT_OPERATORS } },
211
+ });
212
+ ```
213
+
214
+ A fresh filter opens on `meta.filter.defaultOperator` when set, else on the
215
+ type's default when the list holds it, else on the first entry. The lookup at
216
+ the boundary still returns `undefined` for an unmapped operator: a filter
217
+ restored by `persist` from before the list was narrowed can carry one.
218
+
219
+ ### Keying the fetch on the request
220
+
221
+ Serialize the request with `JSON.stringify` inside `useMemo` over
222
+ `columnFilters`, `sorting` and `pagination`, and key the fetch effect (or the
223
+ TanStack Query `queryKey`) on that string. Opening the panel and adding an
224
+ empty row moves `columnFilters` but leaves the request unchanged, so nothing is
225
+ sent. The effect owes the server a debounce (the value input updates on every
226
+ keystroke) and a cancel (a `cancelled` flag in the cleanup, or an
227
+ `AbortController` on a real `fetch`).
228
+
229
+ ### Paging against a page envelope
230
+
231
+ | The API's | The grid's | Written as |
232
+ | --- | --- | --- |
233
+ | `page.number`, counted from 1 | `pagination.pageIndex`, counted from 0 | `number: pageIndex + 1` |
234
+ | `page.totalItems`, the matched count | `rowCount` | `rowCount: page?.totalItems ?? 0` |
235
+ | `page.totalPages` | `state.pageCount`, derived from `rowCount / pageSize` | Nothing; the grid computes it |
236
+
237
+ Forward `totalPages` only when the server pages by something other than the
238
+ size the grid asked for. `pageCount: -1` when the total is unknown. Show the
239
+ page number through the Footer's `renderPagination` slot with
240
+ `<Controls.PageSize /><Controls.PageNumber /><Controls.Pager />`.
241
+ `meta.totalRowCount` is the unfiltered total, which no filtered response
242
+ carries: take it from a separate count call.
243
+
99
244
  ## Persistence
100
245
 
101
246
  `persist` works unchanged. `dataKey` restores filters, sorting and pagination
@@ -126,18 +271,26 @@ Without `rowCount` the table derives the total from the rows it was handed -
126
271
  one page - so `getPageCount()` returns 1. The footer shows "1–25 of 25" and the
127
272
  next-page button is disabled, with no error. Pass the server total.
128
273
 
129
- ### Filters sent without isFilterActive
274
+ ### Filters sent without activeColumnFilters
130
275
 
131
276
  An empty filter value stays in `columnFilters` while the user is still typing.
132
277
  Forwarded verbatim it becomes `operator: "contains", value: ""` at the API,
133
- which most backends translate into a real predicate. Filter with
134
- `isFilterActive` first.
278
+ which most backends translate into a real predicate. Map over
279
+ `activeColumnFilters(columnFilters)` instead of over the slice.
135
280
 
136
281
  ### SummaryCount without meta.totalRowCount
137
282
 
138
- `SummaryCount` falls back to the pre-filtered row count, which under manual
139
- filtering is just the current page. It reads "25 of 25" regardless of how many
140
- rows the server holds. Pass the unfiltered total as `meta.totalRowCount`.
283
+ Without it there is no denominator to show: the pre-filtered row count is the
284
+ current page, so the grid renders the matched count alone rather than a
285
+ plausible-looking wrong total. Pass the unfiltered total as
286
+ `meta.totalRowCount` to get the "42 / 5000" form back.
287
+
288
+ ### Offering operators the endpoint cannot answer
289
+
290
+ The panel offers every operator of the column's type, and a mapping that drops
291
+ `startsWith` leaves the user with a filter that silently does nothing. Declare
292
+ `meta.filter.operators` on the column with the operators the endpoint answers,
293
+ and type the operator table over that same list.
141
294
 
142
295
  ### Unstable getRowId across pages
143
296