@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.
- package/README.md +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
package/skills/rows/SKILL.md
CHANGED
|
@@ -17,14 +17,14 @@ description: >
|
|
|
17
17
|
metadata:
|
|
18
18
|
type: core
|
|
19
19
|
library: '@jielga/tmdatagrid'
|
|
20
|
-
library_version: '2.0.0-beta.
|
|
20
|
+
library_version: '2.0.0-beta.21'
|
|
21
21
|
sources:
|
|
22
|
-
- 'Jielga/TMDataGrid:
|
|
23
|
-
- 'Jielga/TMDataGrid:
|
|
24
|
-
- 'Jielga/TMDataGrid:
|
|
25
|
-
- 'Jielga/TMDataGrid:
|
|
26
|
-
- 'Jielga/TMDataGrid:
|
|
27
|
-
- 'Jielga/TMDataGrid:
|
|
22
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-selection.md'
|
|
23
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-interaction.md'
|
|
24
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-styling.md'
|
|
25
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-details.md'
|
|
26
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-pinning.md'
|
|
27
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/rowSelection.ts'
|
|
28
28
|
---
|
|
29
29
|
|
|
30
30
|
# TMDataGrid - Rows
|
|
@@ -193,8 +193,8 @@ Returning an empty list leaves the column with no menu button at all.
|
|
|
193
193
|
<TMDataGrid.Table<Employee>
|
|
194
194
|
striped
|
|
195
195
|
rowStyle={(row) =>
|
|
196
|
-
row.original.status === "Terminated"
|
|
197
|
-
? { "--row-bg": "var(--mantine-color-red-
|
|
196
|
+
!row.getIsGrouped() && row.original.status === "Terminated"
|
|
197
|
+
? { "--row-bg": "color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent)" }
|
|
198
198
|
: undefined
|
|
199
199
|
}
|
|
200
200
|
rowClassName={(row) => (row.original.overdue ? classes.overdue : undefined)}
|
|
@@ -210,7 +210,11 @@ are not striped.
|
|
|
210
210
|
|
|
211
211
|
Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
|
|
212
212
|
`data-grouped`, `data-depth`, `data-context-menu` and `data-row-id`, so a
|
|
213
|
-
stylesheet can target any of it without a callback.
|
|
213
|
+
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
|
|
216
|
+
reads under both colour schemes - a `-0` Mantine shade is near-white and
|
|
217
|
+
unreadable in dark mode; mix a mid shade into transparency instead.
|
|
214
218
|
|
|
215
219
|
## The details panel
|
|
216
220
|
|
|
@@ -291,7 +295,7 @@ Correct:
|
|
|
291
295
|
rowStyle={() => ({ "--row-bg": "pink" })}
|
|
292
296
|
```
|
|
293
297
|
|
|
294
|
-
Source: `
|
|
298
|
+
Source: `packages/tmdatagrid/docs/row-styling.md` (Set `--row-bg`, not `background`).
|
|
295
299
|
|
|
296
300
|
### CRITICAL Reading the selection without subscribing
|
|
297
301
|
|
|
@@ -313,7 +317,7 @@ const selected = useSelector(grid.table.store, () =>
|
|
|
313
317
|
);
|
|
314
318
|
```
|
|
315
319
|
|
|
316
|
-
Source: `
|
|
320
|
+
Source: `packages/tmdatagrid/docs/row-selection.md` (Acting on a selection).
|
|
317
321
|
|
|
318
322
|
### HIGH Looking for the highlight in `rowSelection`
|
|
319
323
|
|
|
@@ -334,7 +338,7 @@ Correct:
|
|
|
334
338
|
const current = useSelector(grid.ui, (state) => state.highlightedRowId);
|
|
335
339
|
```
|
|
336
340
|
|
|
337
|
-
Source: `
|
|
341
|
+
Source: `packages/tmdatagrid/docs/row-selection.md` (The highlight is not a selection).
|
|
338
342
|
|
|
339
343
|
### HIGH Expecting a click handler to replace the built-in behaviour
|
|
340
344
|
|
|
@@ -349,7 +353,7 @@ Correct, when the click should only navigate:
|
|
|
349
353
|
useTMDataGrid({ data, columns, selectionMode: "highlight" });
|
|
350
354
|
```
|
|
351
355
|
|
|
352
|
-
Source: `
|
|
356
|
+
Source: `packages/tmdatagrid/docs/row-interaction.md`.
|
|
353
357
|
|
|
354
358
|
### HIGH Reading `row.getIsExpanded()` in a cell without subscribing
|
|
355
359
|
|
|
@@ -362,7 +366,7 @@ Correct:
|
|
|
362
366
|
const expanded = useSelector(row.table.store, () => row.getIsExpanded());
|
|
363
367
|
```
|
|
364
368
|
|
|
365
|
-
Source: `
|
|
369
|
+
Source: `packages/tmdatagrid/docs/row-details.md` (Opening a row from elsewhere).
|
|
366
370
|
|
|
367
371
|
### MEDIUM Assuming group rows behave like data rows
|
|
368
372
|
|
|
@@ -371,7 +375,7 @@ Group rows are built on their first child's record. They do not fire
|
|
|
371
375
|
never pin, they take no row number, and they have no details panel. A handler
|
|
372
376
|
written as though every row reaches it silently skips them.
|
|
373
377
|
|
|
374
|
-
Source: `
|
|
378
|
+
Source: `packages/tmdatagrid/docs/row-interaction.md`, `packages/tmdatagrid/docs/row-pinning.md`.
|
|
375
379
|
|
|
376
380
|
### MEDIUM Open panels closing when `data` is replaced
|
|
377
381
|
|
|
@@ -384,7 +388,7 @@ Correct:
|
|
|
384
388
|
useTMDataGrid({ data, columns, renderDetails, autoResetExpanded: false });
|
|
385
389
|
```
|
|
386
390
|
|
|
387
|
-
Source: `
|
|
391
|
+
Source: `packages/tmdatagrid/docs/row-details.md`.
|
|
388
392
|
|
|
389
393
|
### MEDIUM Expecting pinned rows to persist
|
|
390
394
|
|
|
@@ -392,7 +396,7 @@ Source: `src/docs/row-details.md`.
|
|
|
392
396
|
layout store outlives any one data set. A pinned id whose row leaves `data` is
|
|
393
397
|
not shown, and returns to its edge if the data comes back.
|
|
394
398
|
|
|
395
|
-
Source: `
|
|
399
|
+
Source: `packages/tmdatagrid/docs/row-pinning.md`.
|
|
396
400
|
|
|
397
401
|
## References
|
|
398
402
|
|
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
18
|
+
library_version: '2.0.0-beta.21'
|
|
14
19
|
sources:
|
|
15
|
-
- 'Jielga/TMDataGrid:
|
|
16
|
-
- 'Jielga/TMDataGrid:
|
|
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
|
|
89
|
-
|
|
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 {
|
|
119
|
+
import { activeColumnFilters } from "@jielga/tmdatagrid";
|
|
93
120
|
|
|
94
|
-
const active = columnFilters
|
|
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
|
|
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.
|
|
134
|
-
`
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -11,11 +11,11 @@ description: >
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: '@jielga/tmdatagrid'
|
|
14
|
-
library_version: '2.0.0-beta.
|
|
14
|
+
library_version: '2.0.0-beta.21'
|
|
15
15
|
sources:
|
|
16
|
-
- 'Jielga/TMDataGrid:
|
|
17
|
-
- 'Jielga/TMDataGrid:
|
|
18
|
-
- 'Jielga/TMDataGrid:
|
|
16
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
17
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGrid.tsx'
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridTable.tsx'
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
# TMDataGrid - Testing
|
|
@@ -62,8 +62,9 @@ Body cells carry no `data-dg-part` - the coordinate pair already names them.
|
|
|
62
62
|
|
|
63
63
|
**Whole-grid** (unique, no coordinate needed): `toolbar`, `summary-count`,
|
|
64
64
|
`loading`, `search`, `search-clear`, `filter-button`, `filter-panel`,
|
|
65
|
-
`filter-
|
|
66
|
-
`
|
|
65
|
+
`filter-popup`, `filter-sidebar`, `filter-panel-close`, `filter-add`,
|
|
66
|
+
`filter-clear-all`, `filter-pills`, `header-filter-row`,
|
|
67
|
+
`menu-button`, `columns-panel`, `columns-search`, `columns-toggle-all`,
|
|
67
68
|
`columns-reset`, `footer`, `page-size`, `page-range`, `page-prev`, `page-next`,
|
|
68
69
|
`summary-row`, `pinned-top`, `pinned-bottom`, `select-all`,
|
|
69
70
|
`details-toggle-all`, `save-all`, `discard-all`, `editor-confirm`,
|
|
@@ -75,7 +76,9 @@ Body cells carry no `data-dg-part` - the coordinate pair already names them.
|
|
|
75
76
|
`discard-new-row`.
|
|
76
77
|
|
|
77
78
|
**Keyed by `data-column-id`**: `header`, `header-sort`, `header-menu`,
|
|
78
|
-
`header-filter
|
|
79
|
+
`header-filter` (absent under `filters.inHeader`), `header-filter-cell`,
|
|
80
|
+
`header-filter-operator`, `filter-row`,
|
|
81
|
+
`filter-pill`, `columns-toggle`.
|
|
79
82
|
|
|
80
83
|
**Keyed by both**: `editor`.
|
|
81
84
|
|
|
@@ -32,27 +32,15 @@ export function useTMDataGridContext(): TMDataGridContextValue {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* What a control inside a
|
|
35
|
+
* What a control inside a body cell puts in its `tabIndex`. Internal: a
|
|
36
|
+
* consumer's own control needs nothing, the tab guards keep the body one stop.
|
|
36
37
|
*
|
|
37
|
-
* `-1`
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* which reaches a `-1` control perfectly well.
|
|
42
|
-
*
|
|
43
|
-
* Header controls are not covered: the header row is not part of cell
|
|
44
|
-
* navigation, so its sort buttons and menus stay in the tab order, where they
|
|
45
|
-
* are the only way to reach them.
|
|
46
|
-
*
|
|
47
|
-
* A custom cell renderer with a control in it wants the same:
|
|
48
|
-
*
|
|
49
|
-
* ```tsx
|
|
50
|
-
* cell: ({ row }) => (
|
|
51
|
-
* <Button tabIndex={useCellControlTabIndex()} onClick={...}>Open</Button>
|
|
52
|
-
* )
|
|
53
|
-
* ```
|
|
38
|
+
* `-1` under cell selection: the control is reached by stepping into the cell
|
|
39
|
+
* with Enter or by the Tab walk within the row, never by the page's tab order,
|
|
40
|
+
* so a row does not add one tab stop per mounted row. `0` without it - there
|
|
41
|
+
* is no cursor to step in from, so the page's tab order is the only route.
|
|
54
42
|
*/
|
|
55
|
-
export function
|
|
43
|
+
export function useBodyControlTabIndex(): 0 | -1 {
|
|
56
44
|
const { features } = useTMDataGridContext();
|
|
57
45
|
return features.cellSelection ? -1 : 0;
|
|
58
46
|
}
|
|
@@ -82,8 +82,14 @@
|
|
|
82
82
|
/* The stacking ladder, stated once. Body cells sit at auto (0); everything
|
|
83
83
|
sticky stacks over them in this order, and a sticky row crossing a sticky
|
|
84
84
|
column takes the row's slot plus one. */
|
|
85
|
+
/* The focus ring is drawn inside the focused cell's own box, so the cell
|
|
86
|
+
takes a slot of its own - above the plain cells beside it, but under the
|
|
87
|
+
pinned lanes, which a scrolling row has to pass beneath ring and all. A
|
|
88
|
+
focused cell that is itself pinned goes above them instead, so its ring
|
|
89
|
+
stays whole while the rest of the row slides under it. */
|
|
90
|
+
--dg-z-focused-cell: 1;
|
|
85
91
|
--dg-z-pinned-cell: 2;
|
|
86
|
-
--dg-z-focused-cell: 3;
|
|
92
|
+
--dg-z-pinned-focused-cell: 3;
|
|
87
93
|
/* The pinned-row slot serves both sticky rows: the summary along the
|
|
88
94
|
bottom and the entry block under the header. */
|
|
89
95
|
--dg-z-pinned-row: 4;
|
|
@@ -3,20 +3,21 @@ import { type CSSProperties, type ReactNode, useMemo } from "react";
|
|
|
3
3
|
import classes from "./TMDataGrid.module.css";
|
|
4
4
|
import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
|
|
5
5
|
import { TMDataGridContextProvider } from "../TMDataGridContext";
|
|
6
|
-
import {
|
|
6
|
+
import { TMDataGridDraftActions } from "./TMDataGridDraftActions";
|
|
7
7
|
import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
|
|
8
8
|
import { TMDataGridFilterPills } from "./TMDataGridFilterPills";
|
|
9
9
|
import { TMDataGridFooter } from "./TMDataGridFooter";
|
|
10
|
+
import { TMDataGridMenu } from "./TMDataGridMenu";
|
|
10
11
|
import { TMDataGridSearch } from "./TMDataGridSearch";
|
|
11
12
|
import { TMDataGridTable } from "./TMDataGridTable";
|
|
12
13
|
import {
|
|
13
|
-
TMDataGridColumnsButton,
|
|
14
14
|
TMDataGridFilterButton,
|
|
15
15
|
TMDataGridLoadingIndicator,
|
|
16
16
|
TMDataGridSummaryCount,
|
|
17
17
|
TMDataGridToolbar,
|
|
18
18
|
TMDataGridToolbarSpacer,
|
|
19
19
|
} from "./TMDataGridToolbar";
|
|
20
|
+
import { TMDataGridExportPicker } from "./TMDataGridExportPicker";
|
|
20
21
|
import {
|
|
21
22
|
DEFAULT_TMDATAGRID_SIZE,
|
|
22
23
|
SIZE_CONTROL_SIZE,
|
|
@@ -55,29 +56,14 @@ export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
|
|
|
55
56
|
"data-testid"?: string;
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
* Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
|
|
60
|
-
* and publishes it to the compound components below it:
|
|
61
|
-
*
|
|
62
|
-
* ```tsx
|
|
63
|
-
* const grid = useTMDataGrid({ data, columns });
|
|
64
|
-
*
|
|
65
|
-
* <TMDataGrid {...grid}>
|
|
66
|
-
* <TMDataGrid.Toolbar>
|
|
67
|
-
* <TMDataGrid.SummaryCount />
|
|
68
|
-
* <TMDataGrid.Spacer />
|
|
69
|
-
* <TMDataGrid.ColumnsButton />
|
|
70
|
-
* </TMDataGrid.Toolbar>
|
|
71
|
-
* <TMDataGrid.Table />
|
|
72
|
-
* <TMDataGrid.Footer />
|
|
73
|
-
* </TMDataGrid>
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
59
|
+
// Documented on the `TMDataGrid` export below.
|
|
76
60
|
function TMDataGridRoot<TData extends RowData>({
|
|
77
61
|
table,
|
|
78
62
|
ui,
|
|
79
63
|
edit,
|
|
80
64
|
features,
|
|
65
|
+
filters,
|
|
66
|
+
exportOptions,
|
|
81
67
|
labels,
|
|
82
68
|
renderDetails,
|
|
83
69
|
renderDetailsEstHeight,
|
|
@@ -100,6 +86,8 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
100
86
|
ui,
|
|
101
87
|
edit,
|
|
102
88
|
features,
|
|
89
|
+
filters,
|
|
90
|
+
exportOptions,
|
|
103
91
|
labels,
|
|
104
92
|
renderDetails,
|
|
105
93
|
renderDetailsEstHeight,
|
|
@@ -116,6 +104,8 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
116
104
|
ui,
|
|
117
105
|
edit,
|
|
118
106
|
features,
|
|
107
|
+
filters,
|
|
108
|
+
exportOptions,
|
|
119
109
|
labels,
|
|
120
110
|
renderDetails,
|
|
121
111
|
renderDetailsEstHeight,
|
|
@@ -141,18 +131,41 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
141
131
|
>
|
|
142
132
|
{children}
|
|
143
133
|
</div>
|
|
134
|
+
{/* Portaled by Mantine, so outside the root element; inside the
|
|
135
|
+
provider, since it reads the grid. */}
|
|
136
|
+
<TMDataGridExportPicker />
|
|
144
137
|
</TMDataGridContextProvider>
|
|
145
138
|
);
|
|
146
139
|
}
|
|
147
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
|
|
143
|
+
* and publishes it to the compound components below it:
|
|
144
|
+
*
|
|
145
|
+
* ```tsx
|
|
146
|
+
* const grid = useTMDataGrid({ data, columns });
|
|
147
|
+
*
|
|
148
|
+
* <TMDataGrid {...grid}>
|
|
149
|
+
* <TMDataGrid.Toolbar>
|
|
150
|
+
* <TMDataGrid.SummaryCount />
|
|
151
|
+
* <TMDataGrid.Spacer />
|
|
152
|
+
* <TMDataGrid.Menu>
|
|
153
|
+
* <TMDataGrid.Menu.Columns />
|
|
154
|
+
* </TMDataGrid.Menu>
|
|
155
|
+
* </TMDataGrid.Toolbar>
|
|
156
|
+
* <TMDataGrid.Table />
|
|
157
|
+
* <TMDataGrid.Footer />
|
|
158
|
+
* </TMDataGrid>
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
148
161
|
export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
149
162
|
Toolbar: TMDataGridToolbar,
|
|
150
163
|
Spacer: TMDataGridToolbarSpacer,
|
|
151
164
|
SummaryCount: TMDataGridSummaryCount,
|
|
152
165
|
LoadingIndicator: TMDataGridLoadingIndicator,
|
|
153
166
|
Search: TMDataGridSearch,
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
DraftActions: TMDataGridDraftActions,
|
|
168
|
+
Menu: TMDataGridMenu,
|
|
156
169
|
FilterButton: TMDataGridFilterButton,
|
|
157
170
|
Table: TMDataGridTable,
|
|
158
171
|
Footer: TMDataGridFooter,
|
|
@@ -163,6 +176,9 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
|
163
176
|
* rendered outside `<TMDataGrid>` - a page header, for instance.
|
|
164
177
|
*/
|
|
165
178
|
FilterPills: TMDataGridFilterPills,
|
|
166
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* The column chooser as plain controls, for a Popover, a Drawer or an
|
|
181
|
+
* inline layout; `TMDataGrid.Menu.Columns` is the same thing as menu items.
|
|
182
|
+
*/
|
|
167
183
|
ColumnsPanel: TMDataGridColumnsPanel,
|
|
168
184
|
});
|