@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.
- 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 +268 -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 +46 -47
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +67 -40
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +22 -19
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +7 -7
- package/skills/rows/SKILL.md +22 -18
- 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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-editing
|
|
3
|
+
description: >
|
|
4
|
+
Test TMDataGrid editing flows from Playwright: adding a row through the entry
|
|
5
|
+
row, where its temporary id (__new__1) goes at commit and at save, finding an
|
|
6
|
+
added row when the grid does not know the id the app gave it, asserting a
|
|
7
|
+
failed validation on an entry row, cell and row edits, deletions, and the
|
|
8
|
+
draft store (data-new, data-draft, data-dirty, data-deleted, row-state,
|
|
9
|
+
save-all and data-draft-count). Load when writing or fixing a test that adds,
|
|
10
|
+
edits, deletes or saves rows, or when a locator for a new row stops resolving
|
|
11
|
+
after the row is committed.
|
|
12
|
+
metadata:
|
|
13
|
+
type: core
|
|
14
|
+
library: '@jielga/tmdatagrid'
|
|
15
|
+
library_version: '2.0.0'
|
|
16
|
+
sources:
|
|
17
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editing.md'
|
|
19
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/draft-store.md'
|
|
20
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridEntryRows.tsx'
|
|
21
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridEditColumn.tsx'
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# TMDataGrid - Testing editing
|
|
25
|
+
|
|
26
|
+
Builds on the `testing` skill: parts are `[data-dg-part]`, coordinates are
|
|
27
|
+
`[data-row-id]` / `[data-column-id]`, and `DataGrid` is its page object, built
|
|
28
|
+
with `DataGrid.byTestId(page, "orders")` or `new DataGrid(rootLocator)`.
|
|
29
|
+
|
|
30
|
+
## The life of an added row
|
|
31
|
+
|
|
32
|
+
`edit.addRow()` opens an entry row keyed by a temporary id. What happens at ✓
|
|
33
|
+
depends on `editing.draft`, and that decides what a test can hold on to:
|
|
34
|
+
|
|
35
|
+
| Step | Without `draft` | With `draft: true` |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `addRow()` | `[data-dg-part="entry-row"][data-row-id="__new__1"]`; editors are `[data-dg-part="editor"][data-row-id="__new__1"][data-column-id="..."]` | Same |
|
|
38
|
+
| ✓ fails validation | Entry row stays; failing cells carry `data-invalid="true"` | Same |
|
|
39
|
+
| ✓ (`confirm-new-row`) | **Entry row removed, temp id gone.** `onRowAdd({ tempId, value })` runs in the app; the row exists again only when the app puts it in `data`, under the id the app's `getRowId` returns | Becomes a body row **still keyed by the temp id**: `[data-dg-part="row"][data-row-id="__new__1"][data-new="true"][data-draft="true"]`, lane shows `[data-dg-part="row-state"][data-state="new"]`, `[data-dg-part="save-all"]` counts it in `data-draft-count` |
|
|
40
|
+
| Save (`save-all`) | - | As ✓ without `draft`: temp id gone, row comes back through `data` under the app's id |
|
|
41
|
+
| `newRowsSticky: true` | - | The committed row stays an `entry-row` with `data-committed="true"`, outside `data-dg-row-count`, until Save |
|
|
42
|
+
|
|
43
|
+
The grid never learns the id the app assigns (a database id, a negative
|
|
44
|
+
counter, a uuid - whatever `onRowAdd` or `onSaveDrafts` does). The temp id →
|
|
45
|
+
real id mapping does not exist anywhere in the DOM.
|
|
46
|
+
|
|
47
|
+
## The rule: find an added row by content, not by id
|
|
48
|
+
|
|
49
|
+
Once `onRowAdd` or `saveDrafts` has run, the only thing the test knows about
|
|
50
|
+
the row is what it typed. Narrow the grid to a value unique to that row, assert
|
|
51
|
+
that exactly one row is left, read that row's cells:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
async expectRowAdded(uniqueValue: string, cells: Record<string, string>) {
|
|
55
|
+
await this.search(uniqueValue); // or filterBy on a column when there is no quick search
|
|
56
|
+
await this.expectRowCount(1); // retries until the app has put the row in data
|
|
57
|
+
const row = this.part("row"); // exactly one body row now
|
|
58
|
+
for (const [columnId, text] of Object.entries(cells)) {
|
|
59
|
+
await expect(
|
|
60
|
+
row.locator(`[data-column-id="${columnId}"]:not([data-dg-part])`),
|
|
61
|
+
).toHaveText(text);
|
|
62
|
+
}
|
|
63
|
+
await this.search("");
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- The `data-dg-row-count` assertion is the wait. Nothing else signals that
|
|
68
|
+
`onRowAdd` finished: `aria-busy` reflects `meta.loading` only.
|
|
69
|
+
- Use a value no other row has - put a run id in the name. Two matches means
|
|
70
|
+
`expectRowCount(1)` fails, correctly.
|
|
71
|
+
- Scoped to the grid and to one column: no `page.getByText`, no virtualization
|
|
72
|
+
problem, no strict-mode ambiguity.
|
|
73
|
+
|
|
74
|
+
## Page object methods
|
|
75
|
+
|
|
76
|
+
Add these to the `DataGrid` class from the `testing` skill:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
entryRow(): Locator {
|
|
80
|
+
return this.part("entry-row");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Built-in editors of an open row: an entry row, or a row opened in row mode. */
|
|
84
|
+
async fillRow(rowId: string, values: Record<string, string>): Promise<void> {
|
|
85
|
+
for (const [columnId, value] of Object.entries(values)) {
|
|
86
|
+
await this.part("editor", { rowId, columnId })
|
|
87
|
+
.locator('[data-dg-part="editor-input"]')
|
|
88
|
+
.fill(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async commitEntryRow(rowId: string): Promise<void> {
|
|
93
|
+
await this.part("confirm-new-row", { rowId }).click();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Save in TMDataGrid.DraftActions, waited until the store is empty. */
|
|
97
|
+
async saveDrafts(): Promise<void> {
|
|
98
|
+
await this.part("save-all").click();
|
|
99
|
+
await expect(this.part("save-all")).toHaveAttribute("data-draft-count", "0");
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`fillRow` covers the built-in editors. A column with `meta.edit.editor` renders
|
|
104
|
+
the app's own component inside the `editor` part; scope through
|
|
105
|
+
`part("editor", { rowId, columnId })` and fill whatever is inside.
|
|
106
|
+
|
|
107
|
+
## Recipes
|
|
108
|
+
|
|
109
|
+
### Add a row, without draft
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await page.getByRole("button", { name: "Add row" }).click(); // the app's button → edit.addRow()
|
|
113
|
+
const tempId = (await grid.entryRow().getAttribute("data-row-id"))!;
|
|
114
|
+
await grid.fillRow(tempId, { name: "Nordkvist-4711", city: "Stockholm" });
|
|
115
|
+
await grid.commitEntryRow(tempId);
|
|
116
|
+
|
|
117
|
+
await expect(grid.entryRow()).toHaveCount(0); // entry row gone; tempId is now meaningless
|
|
118
|
+
await grid.expectRowAdded("Nordkvist-4711", { name: "Nordkvist-4711", city: "Stockholm" });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Add a row, with draft, then save
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
await grid.commitEntryRow(tempId);
|
|
125
|
+
const row = grid.part("row", { rowId: tempId }); // tempId is still the row's id
|
|
126
|
+
await expect(row).toHaveAttribute("data-new", "true");
|
|
127
|
+
await expect(row).toHaveAttribute("data-draft", "true");
|
|
128
|
+
await expect(grid.part("row-state", { rowId: tempId })).toHaveAttribute("data-state", "new");
|
|
129
|
+
await expect(grid.part("save-all")).toHaveAttribute("data-draft-count", "1");
|
|
130
|
+
|
|
131
|
+
await grid.saveDrafts();
|
|
132
|
+
await expect(row).toHaveCount(0); // tempId dies here
|
|
133
|
+
await grid.expectRowAdded("Nordkvist-4711", { city: "Stockholm" });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Reopening a committed draft row: `edit-row` (the lane's pencil) or a
|
|
137
|
+
double-click puts it back in the entry block, still under the temp id; ✕
|
|
138
|
+
(`discard-new-row`) removes it.
|
|
139
|
+
|
|
140
|
+
### A ✓ that fails validation
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await grid.commitEntryRow(tempId);
|
|
144
|
+
await expect(grid.entryRow()).toHaveCount(1); // still open
|
|
145
|
+
await expect(
|
|
146
|
+
grid.entryRow().locator('[data-column-id="email"]:not([data-dg-part])'),
|
|
147
|
+
).toHaveAttribute("data-invalid", "true");
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The cell and the open `editor` inside it share the column id, so the selector
|
|
151
|
+
leaves the editor out.
|
|
152
|
+
|
|
153
|
+
### Change a cell or a row
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const cell = grid.cell({ rowId: "42", columnId: "salary" });
|
|
157
|
+
await cell.dblclick(); // cell mode: opens the editor
|
|
158
|
+
await grid.fillRow("42", { salary: "52000" });
|
|
159
|
+
await page.keyboard.press("Enter"); // commits
|
|
160
|
+
|
|
161
|
+
// Without draft: onCommit ran, the app applied it, the grid holds nothing
|
|
162
|
+
await expect(cell).toHaveText("52 000");
|
|
163
|
+
|
|
164
|
+
// Under draft: the grid holds it until Save
|
|
165
|
+
await expect(cell).toHaveAttribute("data-dirty", "true");
|
|
166
|
+
await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-dirty", "true");
|
|
167
|
+
await expect(grid.part("row-state", { rowId: "42" })).toHaveAttribute("data-state", "edited");
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Row mode: `edit-row` opens the row, `fillRow` reaches its editors, `save-row`
|
|
171
|
+
commits, `cancel-row` discards. `cellConfirm` mode: `editor-confirm` /
|
|
172
|
+
`editor-cancel` beside the input, or Enter / Escape.
|
|
173
|
+
|
|
174
|
+
### Delete a row
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const before = Number(await grid.grid.getAttribute("data-dg-row-count"));
|
|
178
|
+
await grid.part("delete-row", { rowId: "42" }).click();
|
|
179
|
+
|
|
180
|
+
// Without draft: onRowDelete ran, the app removed it from data
|
|
181
|
+
await grid.expectRowCount(before - 1);
|
|
182
|
+
|
|
183
|
+
// Under draft: marked until Save; restore-row undoes the mark
|
|
184
|
+
await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-deleted", "true");
|
|
185
|
+
await grid.part("restore-row", { rowId: "42" }).click();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Attribute values
|
|
189
|
+
|
|
190
|
+
A state attribute is present, with the value `"true"`, only while it applies:
|
|
191
|
+
`data-new`, `data-draft`, `data-dirty`, `data-deleted` on a body row,
|
|
192
|
+
`data-committed` on an entry row, `data-dirty`, `data-invalid`, `data-editing`
|
|
193
|
+
on a cell. `[data-new]` and `[data-new="true"]` match the same rows,
|
|
194
|
+
`toHaveAttribute("data-new")` and `toHaveAttribute("data-new", "true")` both
|
|
195
|
+
hold, and the negative is `not.toHaveAttribute("data-new")`.
|
|
196
|
+
|
|
197
|
+
## Common mistakes
|
|
198
|
+
|
|
199
|
+
### Looking for the new row's text on the page
|
|
200
|
+
|
|
201
|
+
`page.getByText("Nordkvist")` searches the whole document: other grids, the
|
|
202
|
+
toolbar, a toast. And the row may be virtualized out of the DOM, so the
|
|
203
|
+
locator fails or flakes depending on sort order and viewport. Narrow with
|
|
204
|
+
search or a filter, assert `data-dg-row-count` is 1, read that one row.
|
|
205
|
+
|
|
206
|
+
### Keeping the temp id after ✓ without draft
|
|
207
|
+
|
|
208
|
+
`grid.part("row", { rowId: "__new__1" })` never resolves: without `draft` the
|
|
209
|
+
grid drops the entry and the id at ✓, and the row's real id is the app's. Only
|
|
210
|
+
under `draft: true` does the temp id outlive ✓ - and only until Save.
|
|
211
|
+
|
|
212
|
+
### Waiting for the added row with a timeout
|
|
213
|
+
|
|
214
|
+
The app's `onRowAdd` may be async. `expectRowCount(1)` after narrowing retries
|
|
215
|
+
until the row is in `data`; a `waitForTimeout` either wastes time or is too
|
|
216
|
+
short on CI.
|
|
217
|
+
|
|
218
|
+
### Matching the editor with a cell selector
|
|
219
|
+
|
|
220
|
+
While a cell is open, `[data-row-id="42"][data-column-id="salary"]` matches
|
|
221
|
+
the cell and the `editor` part inside it, and strict mode fails on the pair.
|
|
222
|
+
`DataGrid.cell()` appends `:not([data-dg-part])`; a hand-written locator on a
|
|
223
|
+
row's cells needs the same.
|
|
224
|
+
|
|
225
|
+
### Asserting the negative with `"false"`
|
|
226
|
+
|
|
227
|
+
`toHaveAttribute("data-new", "false")` never passes: a state that does not
|
|
228
|
+
apply is an absent attribute, not `"false"`. Use
|
|
229
|
+
`not.toHaveAttribute("data-new")`, and `:not([data-new])` in CSS.
|
|
230
|
+
|
|
231
|
+
### Asserting a deletion with `toHaveCount(0)`
|
|
232
|
+
|
|
233
|
+
A row outside the viewport has no element either. Assert `data-dg-row-count`
|
|
234
|
+
dropped by one, or, under `draft`, `data-deleted="true"` on the row.
|
|
235
|
+
|
|
236
|
+
### Expecting the grid to show the change without draft
|
|
237
|
+
|
|
238
|
+
Without `draft: true` the grid holds nothing after a commit: no `data-dirty`,
|
|
239
|
+
no `row-state`, `save-all` stays disabled at `data-draft-count="0"`. The
|
|
240
|
+
assertion is the cell's text after the app has applied the change to `data`.
|
|
@@ -7,16 +7,17 @@ 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,15 @@ export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
|
|
|
55
56
|
"data-testid"?: string;
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
60
|
+
// oxlint-disable-next-line react/only-export-components -- exported as the Object.assign compound `TMDataGrid`, which the rule cannot see as a component
|
|
76
61
|
function TMDataGridRoot<TData extends RowData>({
|
|
77
62
|
table,
|
|
78
63
|
ui,
|
|
79
64
|
edit,
|
|
80
65
|
features,
|
|
66
|
+
filters,
|
|
67
|
+
exportOptions,
|
|
81
68
|
labels,
|
|
82
69
|
renderDetails,
|
|
83
70
|
renderDetailsEstHeight,
|
|
@@ -100,6 +87,8 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
100
87
|
ui,
|
|
101
88
|
edit,
|
|
102
89
|
features,
|
|
90
|
+
filters,
|
|
91
|
+
exportOptions,
|
|
103
92
|
labels,
|
|
104
93
|
renderDetails,
|
|
105
94
|
renderDetailsEstHeight,
|
|
@@ -116,6 +105,8 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
116
105
|
ui,
|
|
117
106
|
edit,
|
|
118
107
|
features,
|
|
108
|
+
filters,
|
|
109
|
+
exportOptions,
|
|
119
110
|
labels,
|
|
120
111
|
renderDetails,
|
|
121
112
|
renderDetailsEstHeight,
|
|
@@ -141,10 +132,33 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
141
132
|
>
|
|
142
133
|
{children}
|
|
143
134
|
</div>
|
|
135
|
+
{/* Portaled by Mantine, so outside the root element; inside the
|
|
136
|
+
provider, since it reads the grid. */}
|
|
137
|
+
<TMDataGridExportPicker />
|
|
144
138
|
</TMDataGridContextProvider>
|
|
145
139
|
);
|
|
146
140
|
}
|
|
147
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
|
|
144
|
+
* and publishes it to the compound components below it:
|
|
145
|
+
*
|
|
146
|
+
* ```tsx
|
|
147
|
+
* const grid = useTMDataGrid({ data, columns });
|
|
148
|
+
*
|
|
149
|
+
* <TMDataGrid {...grid}>
|
|
150
|
+
* <TMDataGrid.Toolbar>
|
|
151
|
+
* <TMDataGrid.SummaryCount />
|
|
152
|
+
* <TMDataGrid.Spacer />
|
|
153
|
+
* <TMDataGrid.Menu>
|
|
154
|
+
* <TMDataGrid.Menu.Columns />
|
|
155
|
+
* </TMDataGrid.Menu>
|
|
156
|
+
* </TMDataGrid.Toolbar>
|
|
157
|
+
* <TMDataGrid.Table />
|
|
158
|
+
* <TMDataGrid.Footer />
|
|
159
|
+
* </TMDataGrid>
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
148
162
|
export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
149
163
|
Toolbar: TMDataGridToolbar,
|
|
150
164
|
Spacer: TMDataGridToolbarSpacer,
|
|
@@ -152,7 +166,7 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
|
152
166
|
LoadingIndicator: TMDataGridLoadingIndicator,
|
|
153
167
|
Search: TMDataGridSearch,
|
|
154
168
|
DraftActions: TMDataGridDraftActions,
|
|
155
|
-
|
|
169
|
+
Menu: TMDataGridMenu,
|
|
156
170
|
FilterButton: TMDataGridFilterButton,
|
|
157
171
|
Table: TMDataGridTable,
|
|
158
172
|
Footer: TMDataGridFooter,
|
|
@@ -163,6 +177,9 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
|
163
177
|
* rendered outside `<TMDataGrid>` - a page header, for instance.
|
|
164
178
|
*/
|
|
165
179
|
FilterPills: TMDataGridFilterPills,
|
|
166
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* The column chooser as plain controls, for a Popover, a Drawer or an
|
|
182
|
+
* inline layout; `TMDataGrid.Menu.Columns` is the same thing as menu items.
|
|
183
|
+
*/
|
|
167
184
|
ColumnsPanel: TMDataGridColumnsPanel,
|
|
168
185
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionIcon } from "@mantine/core";
|
|
1
|
+
import { ActionIcon, Tooltip } from "@mantine/core";
|
|
2
2
|
import { FieldApi } from "@tanstack/react-form";
|
|
3
3
|
import type { Cell, Row } from "@tanstack/react-table";
|
|
4
4
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "react";
|
|
12
12
|
import classes from "./TMDataGridTable.module.css";
|
|
13
13
|
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
14
|
+
import { isButtonElement, isInputElement, isNode } from "../core/dom";
|
|
14
15
|
import {
|
|
15
16
|
getEditFieldName,
|
|
16
17
|
normalizeFieldValidate,
|
|
@@ -27,6 +28,7 @@ import { TMDataGridMultiSelectEditor } from "./editors/TMDataGridMultiSelectEdit
|
|
|
27
28
|
import { TMDataGridNumberEditor } from "./editors/TMDataGridNumberEditor";
|
|
28
29
|
import { TMDataGridSelectEditor } from "./editors/TMDataGridSelectEditor";
|
|
29
30
|
import { TMDataGridStringEditor } from "./editors/TMDataGridStringEditor";
|
|
31
|
+
import { useFieldError } from "./editors/editorShared";
|
|
30
32
|
|
|
31
33
|
const BUILT_IN_EDITORS: Record<
|
|
32
34
|
string,
|
|
@@ -138,6 +140,17 @@ export function TMDataGridCellEditor({
|
|
|
138
140
|
});
|
|
139
141
|
useEffect(() => field.mount(), [field]);
|
|
140
142
|
|
|
143
|
+
// The field's message, read here rather than in each editor: the tooltip
|
|
144
|
+
// below is the host's, so a column's own `meta.edit.editor` gets it too.
|
|
145
|
+
const error = useFieldError(field);
|
|
146
|
+
// When that tooltip is up, held here rather than left to Tooltip's own
|
|
147
|
+
// `events`: those bring floating-ui's dismiss interaction with them, which
|
|
148
|
+
// puts an `onKeyDown` on the anchor and stops Escape inside the editor
|
|
149
|
+
// before the host's handler below can cancel on it. A controlled `opened`
|
|
150
|
+
// turns the interaction off, so hover and focus are tracked here instead.
|
|
151
|
+
const [messageHovered, setMessageHovered] = useState(false);
|
|
152
|
+
const [messageFocused, setMessageFocused] = useState(false);
|
|
153
|
+
|
|
141
154
|
// Type-to-edit: the seed replaces the value, as it does in a spreadsheet.
|
|
142
155
|
// Only for the types where keystrokes are the input.
|
|
143
156
|
useEffect(() => {
|
|
@@ -190,7 +203,7 @@ export function TMDataGridCellEditor({
|
|
|
190
203
|
// Enter on the ✓ or ✕ is the button's own press, and does what a click on
|
|
191
204
|
// it does: commit and stay, or cancel. Left to the branch below it would
|
|
192
205
|
// commit and move down, and the same button would mean two things.
|
|
193
|
-
if (event.key === "Enter" && event.target
|
|
206
|
+
if (event.key === "Enter" && isButtonElement(event.target)) {
|
|
194
207
|
event.stopPropagation();
|
|
195
208
|
return;
|
|
196
209
|
}
|
|
@@ -220,7 +233,7 @@ export function TMDataGridCellEditor({
|
|
|
220
233
|
const next = tabbables[index + (event.shiftKey ? -1 : 1)];
|
|
221
234
|
if (index >= 0 && next !== undefined) {
|
|
222
235
|
next.focus();
|
|
223
|
-
if (next
|
|
236
|
+
if (isInputElement(next)) next.select();
|
|
224
237
|
return;
|
|
225
238
|
}
|
|
226
239
|
deferAndClose(event.shiftKey ? "defer-shift-tab" : "defer-tab");
|
|
@@ -234,8 +247,12 @@ export function TMDataGridCellEditor({
|
|
|
234
247
|
* Focus left the editor entirely - a click somewhere else. Under `"cell"`
|
|
235
248
|
* that commits (Sheets): a row the user has walked away from is a row they
|
|
236
249
|
* are done typing into, and where the commit lands is `editing.draft`'s
|
|
237
|
-
* business. A commit
|
|
238
|
-
*
|
|
250
|
+
* business. A refused commit - validation, or `onCommit` rejecting - leaves
|
|
251
|
+
* the editor where it is, invalid, with the value still in it: closed, the
|
|
252
|
+
* cell would render the refused value as if it had landed, and the message
|
|
253
|
+
* would go with the editor. The engine holds the same line when another
|
|
254
|
+
* row is opened over a refused one (see `begin`). The user fixes the value,
|
|
255
|
+
* or Escapes.
|
|
239
256
|
*/
|
|
240
257
|
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
|
|
241
258
|
if (closingRef.current) return;
|
|
@@ -244,7 +261,7 @@ export function TMDataGridCellEditor({
|
|
|
244
261
|
// nothing there; the lane's buttons and the keys end the edit.
|
|
245
262
|
if (isRowShaped) return;
|
|
246
263
|
const next = event.relatedTarget;
|
|
247
|
-
if (next
|
|
264
|
+
if (isNode(next) && event.currentTarget.contains(next)) return;
|
|
248
265
|
closingRef.current = true;
|
|
249
266
|
if (features.editMode === "cellConfirm") {
|
|
250
267
|
// The draft stays, dirty-marked, waiting for its ✓.
|
|
@@ -252,7 +269,8 @@ export function TMDataGridCellEditor({
|
|
|
252
269
|
return;
|
|
253
270
|
}
|
|
254
271
|
void edit.commit(row.id).then((ok) => {
|
|
255
|
-
|
|
272
|
+
// Open for another try: the next blur or key has to be able to commit.
|
|
273
|
+
if (!ok) closingRef.current = false;
|
|
256
274
|
});
|
|
257
275
|
};
|
|
258
276
|
|
|
@@ -289,7 +307,35 @@ export function TMDataGridCellEditor({
|
|
|
289
307
|
onMouseDown={(event) => event.stopPropagation()}
|
|
290
308
|
onDoubleClick={(event) => event.stopPropagation()}
|
|
291
309
|
>
|
|
292
|
-
|
|
310
|
+
{/* The validation message, on the editor rather than under it: inside a
|
|
311
|
+
narrow column Mantine's inline error wraps, grows the row and is
|
|
312
|
+
clipped, so the input keeps the invalid border and the text comes up
|
|
313
|
+
beside it. The editor gets a box of its own because Tooltip needs a
|
|
314
|
+
child that takes a ref, which a `meta.edit.editor` need not forward. */}
|
|
315
|
+
<Tooltip
|
|
316
|
+
label={error}
|
|
317
|
+
opened={error !== undefined && (messageHovered || messageFocused)}
|
|
318
|
+
withArrow
|
|
319
|
+
position="bottom-start"
|
|
320
|
+
>
|
|
321
|
+
<div
|
|
322
|
+
className={classes.cellEditorField}
|
|
323
|
+
onMouseEnter={() => setMessageHovered(true)}
|
|
324
|
+
onMouseLeave={() => setMessageHovered(false)}
|
|
325
|
+
onFocus={() => setMessageFocused(true)}
|
|
326
|
+
onBlur={(event) => {
|
|
327
|
+
// Focus moving inside the editor - into an open dropdown, say - is
|
|
328
|
+
// not focus leaving it.
|
|
329
|
+
const next = event.relatedTarget;
|
|
330
|
+
if (isNode(next) && event.currentTarget.contains(next)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
setMessageFocused(false);
|
|
334
|
+
}}
|
|
335
|
+
>
|
|
336
|
+
<Editor {...args} />
|
|
337
|
+
</div>
|
|
338
|
+
</Tooltip>
|
|
293
339
|
{/* cellConfirm's chrome: the draft only commits through the ✓ (or
|
|
294
340
|
Enter), so the pair sits right beside the input. */}
|
|
295
341
|
{features.editMode === "cellConfirm" && (
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
display: flex;
|
|
13
13
|
flex-direction: column;
|
|
14
14
|
gap: var(--mantine-spacing-sm);
|
|
15
|
-
padding:
|
|
15
|
+
padding: var(--mantine-spacing-sm);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.columnsPanelSearch + * .columnsPanelList {
|
|
19
|
+
padding-top: 0;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
.columnsPanelFooter {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Box,
|
|
3
|
+
type BoxProps,
|
|
2
4
|
Button,
|
|
3
5
|
Checkbox,
|
|
4
6
|
ScrollArea,
|
|
@@ -6,36 +8,42 @@ import {
|
|
|
6
8
|
TextInput,
|
|
7
9
|
Tooltip,
|
|
8
10
|
} from "@mantine/core";
|
|
9
|
-
import { useSelector } from "@tanstack/react-store";
|
|
10
11
|
import { useState } from "react";
|
|
11
12
|
import classes from "./TMDataGridColumnsPanel.module.css";
|
|
12
13
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getColumnLabel,
|
|
16
|
+
showColumnSearch,
|
|
17
|
+
type TMDataGridColumnSearchable,
|
|
18
|
+
} from "../core/columnUtils";
|
|
19
|
+
import { useHideableColumns } from "./useHideableColumns";
|
|
14
20
|
import { SearchIcon } from "./icons";
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
|
-
* The "Manage columns" surface
|
|
18
|
-
*
|
|
19
|
-
*
|
|
23
|
+
* The "Manage columns" surface as plain controls, for a host that is not a
|
|
24
|
+
* menu - a Popover, a Drawer, an inline layout. `TMDataGrid.Menu.Columns` is
|
|
25
|
+
* the same chooser as menu items.
|
|
20
26
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
/** Mantine's style props (`w={320}`, `p="sm"`) are set on the panel block. */
|
|
28
|
+
export type TMDataGridColumnsPanelProps = BoxProps & {
|
|
29
|
+
/**
|
|
30
|
+
* The search box over the list: `"auto"` (the default) from six hideable
|
|
31
|
+
* columns, `true` always, `false` never.
|
|
32
|
+
*/
|
|
33
|
+
searchable?: TMDataGridColumnSearchable;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function TMDataGridColumnsPanel({
|
|
37
|
+
className,
|
|
38
|
+
searchable = "auto",
|
|
39
|
+
...others
|
|
40
|
+
}: TMDataGridColumnsPanelProps = {}) {
|
|
41
|
+
const { labels, controlSize, resetSettings } = useTMDataGridContext();
|
|
23
42
|
const [search, setSearch] = useState("");
|
|
24
43
|
|
|
25
|
-
const columnVisibility =
|
|
26
|
-
|
|
27
|
-
(state) => state.columnVisibility,
|
|
28
|
-
);
|
|
44
|
+
const { columns, columnVisibility, shownCount, setAllVisible } =
|
|
45
|
+
useHideableColumns();
|
|
29
46
|
|
|
30
|
-
// Only what can actually be hidden. A column with `enableHiding: false` is
|
|
31
|
-
// left out rather than listed and disabled: a box that cannot be ticked only
|
|
32
|
-
// invites the question, and every generated lane is one - the checkbox and
|
|
33
|
-
// edit lanes hold the controls the grid needs, the tree column follows the
|
|
34
|
-
// grouping state, the row-number gutter follows `enableRowNumbers`. None of
|
|
35
|
-
// them is a setting.
|
|
36
|
-
const columns = table
|
|
37
|
-
.getAllLeafColumns()
|
|
38
|
-
.filter((column) => column.getCanHide());
|
|
39
47
|
const needle = search.trim().toLowerCase();
|
|
40
48
|
const visibleInPanel = needle
|
|
41
49
|
? columns.filter((column) =>
|
|
@@ -43,41 +51,25 @@ export function TMDataGridColumnsPanel() {
|
|
|
43
51
|
)
|
|
44
52
|
: columns;
|
|
45
53
|
|
|
46
|
-
const shownCount = columns.filter(
|
|
47
|
-
(column) => columnVisibility[column.id] !== false,
|
|
48
|
-
).length;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Show or hide every listed column.
|
|
52
|
-
*
|
|
53
|
-
* Not `table.toggleAllColumnsVisible`, which writes a visibility entry for
|
|
54
|
-
* *every* leaf column: showing all would publish the tree column - hidden
|
|
55
|
-
* because nothing is grouped, not because the user hid it - and hiding all
|
|
56
|
-
* would force the same column visible, since it writes `!getCanHide()` for
|
|
57
|
-
* the columns it will not touch. Either way a lane the panel never listed
|
|
58
|
-
* changes state, and persistence then keeps it that way.
|
|
59
|
-
*/
|
|
60
|
-
const setAllVisible = (visible: boolean) => {
|
|
61
|
-
table.setColumnVisibility((previous) => {
|
|
62
|
-
const next = { ...previous };
|
|
63
|
-
for (const column of columns) next[column.id] = visible;
|
|
64
|
-
return next;
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
|
|
68
54
|
return (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
55
|
+
<Box
|
|
56
|
+
data-dg-part="columns-panel"
|
|
57
|
+
className={[classes.columnsPanel, className].filter(Boolean).join(" ")}
|
|
58
|
+
{...others}
|
|
59
|
+
>
|
|
60
|
+
{showColumnSearch(searchable, columns.length) && (
|
|
61
|
+
<div className={classes.columnsPanelSearch}>
|
|
62
|
+
<TextInput
|
|
63
|
+
value={search}
|
|
64
|
+
onChange={(event) => setSearch(event.currentTarget.value)}
|
|
65
|
+
placeholder={labels.columnsSearchPlaceholder}
|
|
66
|
+
leftSection={<SearchIcon size={16} stroke={1.6} />}
|
|
67
|
+
size={controlSize}
|
|
68
|
+
data-dg-part="columns-search"
|
|
69
|
+
data-autofocus
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
81
73
|
|
|
82
74
|
<ScrollArea.Autosize mah={260} type="auto">
|
|
83
75
|
<div className={classes.columnsPanelList}>
|
|
@@ -126,6 +118,6 @@ export function TMDataGridColumnsPanel() {
|
|
|
126
118
|
</Button>
|
|
127
119
|
</Tooltip>
|
|
128
120
|
</div>
|
|
129
|
-
</
|
|
121
|
+
</Box>
|
|
130
122
|
);
|
|
131
123
|
}
|