@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1281 -768
- package/dist/index.js +4607 -3250
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +269 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -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/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -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 +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +49 -48
- package/skills/columns/SKILL.md +125 -70
- package/skills/columns/references/columns-api.md +59 -0
- package/skills/data/SKILL.md +112 -18
- package/skills/editing/SKILL.md +76 -42
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +25 -20
- package/skills/editing/references/editors-and-validation.md +80 -18
- package/skills/filtering/SKILL.md +155 -41
- package/skills/getting-started/SKILL.md +116 -16
- package/skills/grouping/SKILL.md +31 -16
- package/skills/migrating-to-2/SKILL.md +244 -0
- package/skills/options/SKILL.md +24 -12
- package/skills/rows/SKILL.md +22 -18
- package/skills/rows/references/rows-api.md +10 -6
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
- 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 +5 -51
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
- 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 +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -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/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- 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 +16 -2
- 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/components/generatedColumns.tsx +187 -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}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
- package/src/core/export.ts +704 -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}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +69 -35
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
- /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}/capabilities.ts +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}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.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}/resizePreview.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/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -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.
|
|
18
|
+
library_version: '2.0.1'
|
|
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
|
@@ -4,18 +4,21 @@ description: >
|
|
|
4
4
|
Write tests against TMDataGrid from a consuming app - Playwright or React
|
|
5
5
|
Testing Library. Covers the data-dg-part contract, data-row-id/data-column-id
|
|
6
6
|
coordinates, naming a grid with data-testid, the roles and ARIA the grid
|
|
7
|
-
publishes, the cell/gridcell role flip under cell selection,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
publishes, the cell/gridcell role flip under cell selection, the surfaces that
|
|
8
|
+
render in a portal (the menu, the export picker, Select listboxes), reaching
|
|
9
|
+
rows past virtualization with data-dg-row-count, scrollToRow and
|
|
10
|
+
data-dg-scroll-container, waiting on aria-busy, and the DataGrid page object.
|
|
11
|
+
Load when writing or fixing tests that drive a grid, or when a selector for a
|
|
12
|
+
row, cell or control does not resolve.
|
|
11
13
|
metadata:
|
|
12
14
|
type: core
|
|
13
15
|
library: '@jielga/tmdatagrid'
|
|
14
|
-
library_version: '2.0.
|
|
16
|
+
library_version: '2.0.1'
|
|
15
17
|
sources:
|
|
16
|
-
- 'Jielga/TMDataGrid:
|
|
17
|
-
- 'Jielga/TMDataGrid:
|
|
18
|
-
- 'Jielga/TMDataGrid:
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
19
|
+
- 'Jielga/TMDataGrid:playwright/support/DataGrid.ts'
|
|
20
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGrid.tsx'
|
|
21
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridTable.tsx'
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
# TMDataGrid - Testing
|
|
@@ -33,6 +36,11 @@ The grid mints no `data-testid` of its own - that attribute belongs to the app,
|
|
|
33
36
|
and Playwright's `testIdAttribute` is configurable. `@mantine/core` and
|
|
34
37
|
`@tanstack/*` ship none either.
|
|
35
38
|
|
|
39
|
+
Adding, changing and deleting rows, the draft store, and where a new row's
|
|
40
|
+
temporary id goes at commit are in the `testing-editing` skill. Running the grid
|
|
41
|
+
in a real browser through Playwright's `mount` fixture - virtualization, resize,
|
|
42
|
+
drag, pinning, clipboard - is in the `testing-components` skill.
|
|
43
|
+
|
|
36
44
|
## Naming a grid
|
|
37
45
|
|
|
38
46
|
Parts repeat across grids on a page. Name the grid, scope through it:
|
|
@@ -52,70 +60,134 @@ accessible name belongs to the element carrying the `grid` role.
|
|
|
52
60
|
| Element | Role | Key attributes |
|
|
53
61
|
| --- | --- | --- |
|
|
54
62
|
| Grid | `table`, or `grid` under cell selection | `aria-rowcount`, `aria-colcount`, `aria-busy`, `data-dg-row-count` |
|
|
63
|
+
| Scroll container | - | `data-dg-scroll-container` - the element to scroll |
|
|
55
64
|
| Body row | `row` | `data-row-id`, `aria-rowindex`, `data-selected`, `data-highlighted`, `data-grouped`, `data-pinned`, `data-deleted` |
|
|
56
65
|
| Body cell | `cell`, or `gridcell` under cell selection | `data-row-id`, `data-column-id`, `data-editing`, `data-dirty`, `data-invalid`, `data-focused` |
|
|
57
66
|
| Header cell | `columnheader` | `data-column-id`, `aria-sort` |
|
|
58
67
|
|
|
59
68
|
Body cells carry no `data-dg-part` - the coordinate pair already names them.
|
|
69
|
+
The `editor` part of an open cell carries the same pair, so a cell locator is
|
|
70
|
+
`[data-row-id="42"][data-column-id="total"]:not([data-dg-part])`; without the
|
|
71
|
+
exclusion it matches two elements while the cell is being edited.
|
|
72
|
+
|
|
73
|
+
State attributes are present only while they apply: `data-selected`,
|
|
74
|
+
`data-new`, `data-invalid` and the rest are rendered as `"true"` while the
|
|
75
|
+
state holds and omitted otherwise, so `[data-new]` and `[data-new="true"]`
|
|
76
|
+
match the same rows, and the negative is `:not([data-new])` in CSS and
|
|
77
|
+
`not.toHaveAttribute("data-new")` in a test.
|
|
60
78
|
|
|
61
79
|
## Parts
|
|
62
80
|
|
|
63
81
|
**Whole-grid** (unique, no coordinate needed): `toolbar`, `summary-count`,
|
|
64
82
|
`loading`, `search`, `search-clear`, `filter-button`, `filter-panel`,
|
|
65
|
-
`filter-
|
|
66
|
-
`
|
|
67
|
-
`
|
|
68
|
-
`
|
|
69
|
-
`
|
|
70
|
-
`
|
|
83
|
+
`filter-popup`, `filter-sidebar`, `filter-panel-close`, `filter-add`,
|
|
84
|
+
`filter-clear-all`, `filter-pills`, `header-filter-row`,
|
|
85
|
+
`menu-button`, `menu-export`, `menu-export-selected`, `export-picker`,
|
|
86
|
+
`export-picker-search`, `export-picker-hint`, `export-picker-count`,
|
|
87
|
+
`export-column-all`, `export-picker-confirm`, `export-picker-cancel`,
|
|
88
|
+
`columns-panel`, `columns-search`, `columns-toggle-all`,
|
|
89
|
+
`columns-reset`, `footer`, `page-size`, `page-range`, `page-number`,
|
|
90
|
+
`page-prev`, `page-next`, `summary-row`, `pinned-top`, `pinned-bottom`,
|
|
91
|
+
`select-all`, `details-toggle-all`, `save-all`, `discard-all`,
|
|
92
|
+
`editor-confirm`, `editor-cancel`, `editor-input`, `sort-index`, `tab-guard`.
|
|
71
93
|
|
|
72
94
|
**Keyed by `data-row-id`**: `row`, `entry-row`, `details`, `select-row`,
|
|
73
95
|
`details-toggle`, `group-toggle`, `edit-row`, `delete-row`, `save-row`,
|
|
74
96
|
`cancel-row`, `row-state`, `revert-row`, `restore-row`, `confirm-new-row`,
|
|
75
|
-
`discard-new-row`.
|
|
97
|
+
`discard-new-row`, `open-rows-note`.
|
|
76
98
|
|
|
77
99
|
**Keyed by `data-column-id`**: `header`, `header-sort`, `header-menu`,
|
|
78
|
-
`header-
|
|
100
|
+
`header-resize` (the resize handle; present only when the column can resize),
|
|
101
|
+
`header-filter` (absent under `filters.inHeader`), `header-filter-cell`,
|
|
102
|
+
`header-filter-operator`, `filter-row`, `filter-pill`, `columns-toggle`,
|
|
103
|
+
`export-column`.
|
|
79
104
|
|
|
80
105
|
**Keyed by both**: `editor`.
|
|
81
106
|
|
|
82
107
|
Inside a `filter-row` the controls are `filter-column`, `filter-operator` and
|
|
83
|
-
`filter-value` - or `filter-value-from` / `filter-value-to` for `between
|
|
108
|
+
`filter-value` - or `filter-value-from` / `filter-value-to` for `between` - and
|
|
109
|
+
`filter-remove` is its ✕. None of them carries `data-column-id`; scope through
|
|
110
|
+
the row. Inside a `filter-pill`, `filter-pill-remove` is the ✕.
|
|
111
|
+
`TMDataGridFilterPills` renders where you place it; outside the root, scope
|
|
112
|
+
through its own container rather than through the grid.
|
|
84
113
|
|
|
85
114
|
A column declaring `meta.filter.control` or `meta.edit.editor` renders your own
|
|
86
115
|
component in that slot, so `filter-value` and `editor-input` cover the built-ins
|
|
87
116
|
only. `filter-row` and `editor` still hold; scope through them.
|
|
88
117
|
|
|
118
|
+
Sorting: click `header`. `header-sort` is hidden until the header is hovered;
|
|
119
|
+
it shows the state, and `aria-sort` on the header is the assertion.
|
|
120
|
+
|
|
121
|
+
## Portals
|
|
122
|
+
|
|
123
|
+
These surfaces render at the end of `<body>`, outside the grid's root, so a
|
|
124
|
+
locator scoped to the root never finds them:
|
|
125
|
+
|
|
126
|
+
- the `TMDataGrid.Menu` dropdown - `page.getByRole("menu")`, holding
|
|
127
|
+
`columns-toggle`, `columns-toggle-all`, `columns-reset`, `menu-export`,
|
|
128
|
+
`menu-export-selected`
|
|
129
|
+
- a column's menu, opened by `header-menu` (hover the header first) or a right
|
|
130
|
+
click on the header - `page.getByRole("menu")`; its items (sort, filter,
|
|
131
|
+
group, pin, hide) carry no part, so reach one by role and label,
|
|
132
|
+
`menu.getByRole("menuitem", { name: "Group by Location" })` - a translated
|
|
133
|
+
label
|
|
134
|
+
- the `header-filter-operator` menu - `page.getByRole("menu")`
|
|
135
|
+
- the export column picker - `page.getByRole("dialog")`, holding the
|
|
136
|
+
`export-*` parts
|
|
137
|
+
- the listbox of every `Select` or `MultiSelect` the grid renders -
|
|
138
|
+
`page-size`, `filter-column`, `filter-operator`, and the `filter-value` of a
|
|
139
|
+
boolean or select-type filter - the element named by the input's
|
|
140
|
+
`aria-controls`; each option carries its value in `value`
|
|
141
|
+
|
|
142
|
+
One menu or dialog is open at a time, so the page-level locator is unambiguous.
|
|
143
|
+
`filter-popup` and `filter-sidebar` render inside the root.
|
|
144
|
+
|
|
89
145
|
## A page object
|
|
90
146
|
|
|
147
|
+
The full class is on the Testing docs page and in the repository at
|
|
148
|
+
`playwright/support/DataGrid.ts`; it is the one the grid's own suite runs
|
|
149
|
+
against the docs demos. The shape:
|
|
150
|
+
|
|
91
151
|
```ts
|
|
92
152
|
import { type Locator, type Page, expect } from "@playwright/test";
|
|
93
153
|
|
|
94
154
|
type PartKey = { rowId?: string; columnId?: string };
|
|
95
155
|
|
|
96
156
|
export class DataGrid {
|
|
157
|
+
readonly page: Page;
|
|
97
158
|
readonly root: Locator;
|
|
98
159
|
readonly grid: Locator;
|
|
99
160
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
161
|
+
/** `root` is the element carrying `data-dg-root`. */
|
|
162
|
+
constructor(root: Locator) {
|
|
163
|
+
this.page = root.page();
|
|
164
|
+
this.root = root;
|
|
165
|
+
this.grid = root.getByRole("table").or(root.getByRole("grid"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static byTestId(page: Page, testId: string): DataGrid {
|
|
169
|
+
return new DataGrid(page.getByTestId(testId));
|
|
103
170
|
}
|
|
104
171
|
|
|
105
172
|
part(name: string, key: PartKey = {}): Locator {
|
|
106
|
-
return this.root.locator(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
173
|
+
return this.root.locator(partSelector(name, key));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A part inside the open menu dropdown, which renders in a portal. */
|
|
177
|
+
menuPart(name: string, key: PartKey = {}): Locator {
|
|
178
|
+
return this.page.getByRole("menu").locator(partSelector(name, key));
|
|
111
179
|
}
|
|
112
180
|
|
|
113
181
|
cell({ rowId, columnId }: { rowId: string; columnId: string }): Locator {
|
|
114
182
|
return this.root.locator(
|
|
115
|
-
`[data-row-id="${rowId}"][data-column-id="${columnId}"]`,
|
|
183
|
+
`[data-row-id="${rowId}"][data-column-id="${columnId}"]:not([data-dg-part])`,
|
|
116
184
|
);
|
|
117
185
|
}
|
|
118
186
|
|
|
187
|
+
async sortBy(columnId: string): Promise<void> {
|
|
188
|
+
await this.part("header", { columnId }).click();
|
|
189
|
+
}
|
|
190
|
+
|
|
119
191
|
async expectRowCount(count: number): Promise<void> {
|
|
120
192
|
await expect(this.grid).toHaveAttribute("data-dg-row-count", String(count));
|
|
121
193
|
}
|
|
@@ -126,6 +198,31 @@ export class DataGrid {
|
|
|
126
198
|
}
|
|
127
199
|
```
|
|
128
200
|
|
|
201
|
+
The full class adds `search`, `filterBy` (adds a filter row for the column
|
|
202
|
+
when the panel has none; fills the built-in text and number inputs, so a
|
|
203
|
+
select-type filter takes `chooseOption` on its `filter-value` instead),
|
|
204
|
+
`toggleColumn`, `openColumnMenu` (hovers the header, clicks `header-menu`,
|
|
205
|
+
returns the menu), `chooseOption` (an option of a portaled `Select`, by value),
|
|
206
|
+
and the editing methods of the `testing-editing` skill.
|
|
207
|
+
|
|
208
|
+
## Recipes
|
|
209
|
+
|
|
210
|
+
`grid` is a `DataGrid`; the parts are the steps, the attributes are the proof.
|
|
211
|
+
|
|
212
|
+
| Interaction | Steps | Assertion |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| Quick search | `grid.search("Cecilia")`; `search-clear` | `grid.expectRowCount(10)`, then the full count |
|
|
215
|
+
| Sort | `grid.sortBy("lastName")` once, then again | `aria-sort` on `header`: `ascending`, then `descending` |
|
|
216
|
+
| Filter | `grid.filterBy({ columnId, value })`; `filter-clear-all` | `grid.expectRowCount(n)`, then the full count |
|
|
217
|
+
| Remove a filter | `filter-remove` in its `filter-row`, or `filter-pill-remove` in its `filter-pill` | the `filter-pill` has count 0; the row count |
|
|
218
|
+
| Hide a column | `grid.toggleColumn("location")`; `grid.menuPart("columns-reset")` | the `header` has count 0, then is visible |
|
|
219
|
+
| Page | `page-next`; `grid.chooseOption({ select: grid.part("page-size"), value: "50" })` | `page-range` text changes, first `row` has a new `data-row-id`; `grid.expectRowCount(50)` |
|
|
220
|
+
| Select rows | `select-row` of a row; `select-all` | `data-selected="true"` on the `row`; `[data-dg-part="row"]:not([data-selected])` has count 0 |
|
|
221
|
+
| Group | `grid.openColumnMenu("location")`, the "Group by" item; `group-toggle` of a group row | rows carry `data-grouped`; `data-dg-row-count` grows on expand, shrinks on collapse |
|
|
222
|
+
| Load more | scroll `data-dg-scroll-container` to `scrollHeight` | `data-dg-row-count` grows; `grid.expectSettled()` |
|
|
223
|
+
| Export | `menu-button`, then `menu-export` in the menu | `page.waitForEvent("download")`, `download.suggestedFilename()` |
|
|
224
|
+
| Edit a cell | `grid.cell(...).dblclick()`, `grid.fillRow(rowId, { columnId: value })`, Enter | the cell's text; Escape instead of Enter leaves it unchanged |
|
|
225
|
+
|
|
129
226
|
## Virtualization
|
|
130
227
|
|
|
131
228
|
Only the rows in the viewport plus overscan are in the DOM. A row at index 500
|
|
@@ -138,7 +235,10 @@ otherwise. (`aria-rowcount` also counts the header and summary rows.)
|
|
|
138
235
|
**Reach a row by narrowing to it** - filter or search. Faster, more stable, and
|
|
139
236
|
what a user would do. Where the row must be reached in place,
|
|
140
237
|
`grid.scrollToRow({ rowId, align })` moves the virtualizer and answers whether
|
|
141
|
-
the row was reachable; from Playwright that needs the app to expose the api
|
|
238
|
+
the row was reachable; from Playwright that needs the app to expose the api on
|
|
239
|
+
`window`. Scrolling the element carrying `data-dg-scroll-container` moves the
|
|
240
|
+
virtualizer without it, and is also how an infinite-scroll grid is made to
|
|
241
|
+
load its next page.
|
|
142
242
|
|
|
143
243
|
## Waiting
|
|
144
244
|
|
|
@@ -165,14 +265,32 @@ layout, so the count depends on the stubbed element size. Assert
|
|
|
165
265
|
### getByRole("cell") on a grid with cell selection
|
|
166
266
|
|
|
167
267
|
`cellSelection` turns every `cell` into a `gridcell`, and the grid's `table`
|
|
168
|
-
into a `grid
|
|
169
|
-
Tests written on the role break when the feature is switched on. Query cells by
|
|
268
|
+
into a `grid`. Tests written on the role break when the feature is switched on. Query cells by
|
|
170
269
|
`[data-row-id][data-column-id]` instead.
|
|
171
270
|
|
|
271
|
+
### Matching a cell while it is edited
|
|
272
|
+
|
|
273
|
+
`[data-row-id="42"][data-column-id="total"]` matches the cell and the `editor`
|
|
274
|
+
inside it once the cell is open, and strict mode fails on the pair. Add
|
|
275
|
+
`:not([data-dg-part])`.
|
|
276
|
+
|
|
277
|
+
### Clicking header-sort to sort
|
|
278
|
+
|
|
279
|
+
The arrow is `display: none` until the header is hovered, so the click waits
|
|
280
|
+
for visibility and times out. Click `header`; `aria-sort` on it is the result.
|
|
281
|
+
|
|
282
|
+
### Reaching the menu through the root
|
|
283
|
+
|
|
284
|
+
`orders.locator('[data-dg-part="columns-toggle"]')` never resolves: the
|
|
285
|
+
dropdown renders in a portal at the end of `<body>`. Use
|
|
286
|
+
`page.getByRole("menu")` after `menu-button` is clicked. The same goes for the
|
|
287
|
+
export picker (`dialog`) and every `Select` listbox (`aria-controls`).
|
|
288
|
+
|
|
172
289
|
### Expecting a row far down the list to exist
|
|
173
290
|
|
|
174
|
-
`
|
|
175
|
-
with no useful message. Filter or search down to it first
|
|
291
|
+
`[data-row-id="450"]` has no element until it is scrolled to, so the locator times out
|
|
292
|
+
with no useful message. Filter or search down to it first, or scroll
|
|
293
|
+
`data-dg-scroll-container`.
|
|
176
294
|
|
|
177
295
|
### Unscoped parts with two grids on a page
|
|
178
296
|
|
|
@@ -196,4 +314,4 @@ different column than it did. Use `[data-column-id]`.
|
|
|
196
314
|
|
|
197
315
|
`data-dg-part="loading"` only renders where `TMDataGrid.LoadingIndicator` was
|
|
198
316
|
placed, and only while `meta.loading` is true. `aria-busy` on the grid is set
|
|
199
|
-
regardless of whether that component is rendered.
|
|
317
|
+
regardless of whether that component is rendered.
|