@jielga/tmdatagrid 2.0.0-beta.8 → 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 +1323 -796
- package/dist/index.js +4719 -3193
- 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 +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- 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 +8 -8
- 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/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- 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 +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- 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 +9 -72
- 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 +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- 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}/capabilities.ts +5 -5
- 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 +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- 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} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- 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/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}/controlledState.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}/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
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { useCreateStore } from "@tanstack/react-store";
|
|
1
|
+
import { useCreateStore, useSelector } from "@tanstack/react-store";
|
|
2
|
+
import { shallow } from "@tanstack/store";
|
|
2
3
|
import {
|
|
4
|
+
type AccessorFn,
|
|
5
|
+
type AccessorFnColumnDef,
|
|
6
|
+
type AccessorKeyColumnDef,
|
|
3
7
|
aggregationFns,
|
|
4
8
|
type ColumnDef,
|
|
9
|
+
type ColumnHelper,
|
|
5
10
|
columnFacetingFeature,
|
|
6
11
|
columnFilteringFeature,
|
|
7
12
|
columnGroupingFeature,
|
|
@@ -18,11 +23,17 @@ import {
|
|
|
18
23
|
createFilteredRowModel,
|
|
19
24
|
createGroupedRowModel,
|
|
20
25
|
createPaginatedRowModel,
|
|
26
|
+
type DeepKeys,
|
|
27
|
+
type DeepValue,
|
|
28
|
+
type DisplayColumnDef,
|
|
21
29
|
filterFns,
|
|
30
|
+
type GroupColumnDef,
|
|
22
31
|
globalFilteringFeature,
|
|
32
|
+
type IdentifiedColumnDef,
|
|
23
33
|
metaHelper,
|
|
24
34
|
type Row,
|
|
25
35
|
type RowData,
|
|
36
|
+
rowAggregationFeature,
|
|
26
37
|
rowExpandingFeature,
|
|
27
38
|
rowPaginationFeature,
|
|
28
39
|
rowPinningFeature,
|
|
@@ -52,7 +63,7 @@ import {
|
|
|
52
63
|
type TMDataGridEditApi,
|
|
53
64
|
type TMDataGridEditCommitArgs,
|
|
54
65
|
type TMDataGridSaveDraftsArgs,
|
|
55
|
-
type
|
|
66
|
+
type TMDataGridSaveDraftsResponse,
|
|
56
67
|
type TMDataGridEditEngineContext,
|
|
57
68
|
type TMDataGridColumnEditOptions,
|
|
58
69
|
type TMDataGridEditMode,
|
|
@@ -68,6 +79,11 @@ import {
|
|
|
68
79
|
} from "./core/filterOperators";
|
|
69
80
|
import { getColumnDefaultOperator, isControlColumn } from "./core/columnUtils";
|
|
70
81
|
import type { TMDataGridColumnFilterOptions } from "./core/filterControls";
|
|
82
|
+
import {
|
|
83
|
+
resolveFilterOptions,
|
|
84
|
+
type TMDataGridFiltersOptions,
|
|
85
|
+
type TMDataGridFiltersSettings,
|
|
86
|
+
} from "./core/filterSurface";
|
|
71
87
|
import {
|
|
72
88
|
createFuzzyRankedSortedRowModel,
|
|
73
89
|
fuzzyGlobalFilterFn,
|
|
@@ -100,23 +116,34 @@ import {
|
|
|
100
116
|
stabilizeControlledState,
|
|
101
117
|
withoutUndefinedSlices,
|
|
102
118
|
} from "./core/controlledState";
|
|
103
|
-
import type { TMDataGridCellRange } from "./core/cellRange";
|
|
104
119
|
import {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
beginControlledStateSync,
|
|
121
|
+
deferControlledStateSyncPublishes,
|
|
122
|
+
endControlledStateSync,
|
|
123
|
+
} from "./core/controlledStateSync";
|
|
124
|
+
import { registerDeletedRows } from "./core/deletedRows";
|
|
108
125
|
import {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} from "./
|
|
126
|
+
withPageReset,
|
|
127
|
+
type TMDataGridQueryTable,
|
|
128
|
+
} from "./core/pageReset";
|
|
129
|
+
import type { TMDataGridCellRange } from "./core/cellRange";
|
|
112
130
|
import {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
resolveExportOptions,
|
|
132
|
+
type TMDataGridExportOptions,
|
|
133
|
+
type TMDataGridExportPickerRequest,
|
|
134
|
+
type TMDataGridExportSettings,
|
|
135
|
+
type TMDataGridExportValueGetter,
|
|
136
|
+
} from "./core/export";
|
|
137
|
+
import { SELECT_COLUMN_ID } from "./components/TMDataGridSelectColumn";
|
|
138
|
+
import { GROUP_COLUMN_ID } from "./components/TMDataGridGroupColumn";
|
|
139
|
+
import { DETAILS_COLUMN_ID } from "./components/TMDataGridDetailsColumn";
|
|
140
|
+
import { EDIT_COLUMN_ID } from "./components/TMDataGridEditColumn";
|
|
116
141
|
import {
|
|
142
|
+
createDetailsColumn,
|
|
117
143
|
createEditColumn,
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
createGroupColumn,
|
|
145
|
+
createSelectColumn,
|
|
146
|
+
} from "./components/generatedColumns";
|
|
120
147
|
import {
|
|
121
148
|
createRowNumberColumn,
|
|
122
149
|
ROW_NUMBER_COLUMN_ID,
|
|
@@ -138,8 +165,11 @@ const PERSIST_DEBOUNCE_MS = 200;
|
|
|
138
165
|
* `type` and `options` are read by both stages, so one declaration of each
|
|
139
166
|
* feeds the filter panel and the cell editor, which is why they sit outside
|
|
140
167
|
* both namespaces.
|
|
168
|
+
*
|
|
169
|
+
* `TData` types the row that `options` and `edit.enabled` callbacks receive.
|
|
170
|
+
* `createTMDataGridColumnHelper<TData>()` fills it in.
|
|
141
171
|
*/
|
|
142
|
-
export type TMDataGridColumnMeta = {
|
|
172
|
+
export type TMDataGridColumnMeta<TData extends RowData = TMDataGridRowData> = {
|
|
143
173
|
/** Name shown in menus and the column manager. Falls back to a string header. */
|
|
144
174
|
label?: string;
|
|
145
175
|
/**
|
|
@@ -154,7 +184,7 @@ export type TMDataGridColumnMeta = {
|
|
|
154
184
|
* function of the table, column and, for editors, the row. See
|
|
155
185
|
* {@link TMDataGridOptionsSource}.
|
|
156
186
|
*/
|
|
157
|
-
options?: TMDataGridOptionsSource
|
|
187
|
+
options?: TMDataGridOptionsSource<TData>;
|
|
158
188
|
/** Share of the leftover width this column claims. Defaults to `1`. */
|
|
159
189
|
flex?: number;
|
|
160
190
|
align?: "left" | "right" | "center";
|
|
@@ -171,8 +201,8 @@ export type TMDataGridColumnMeta = {
|
|
|
171
201
|
*/
|
|
172
202
|
enableOrdering?: boolean;
|
|
173
203
|
/**
|
|
174
|
-
* How this column filters: the operator a fresh
|
|
175
|
-
* value control the filter panel renders for it.
|
|
204
|
+
* How this column filters: which operators it offers, the operator a fresh
|
|
205
|
+
* filter starts with, and the value control the filter panel renders for it.
|
|
176
206
|
*
|
|
177
207
|
* ```tsx
|
|
178
208
|
* meta: {
|
|
@@ -198,7 +228,26 @@ export type TMDataGridColumnMeta = {
|
|
|
198
228
|
*
|
|
199
229
|
* See {@link TMDataGridColumnEditOptions}.
|
|
200
230
|
*/
|
|
201
|
-
edit?: TMDataGridColumnEditOptions
|
|
231
|
+
edit?: TMDataGridColumnEditOptions<TData>;
|
|
232
|
+
/**
|
|
233
|
+
* `false` leaves the column out of every export and out of Ctrl+C - for a
|
|
234
|
+
* column of buttons, or one whose value means nothing outside the grid.
|
|
235
|
+
* Defaults to `true`.
|
|
236
|
+
*/
|
|
237
|
+
enableExport?: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* The value an export writes for this column, in place of
|
|
240
|
+
* `row.getValue(column.id)`. The export otherwise writes the value, never
|
|
241
|
+
* what the cell renders, so this is where a status code becomes its label
|
|
242
|
+
* or a nested object becomes one field.
|
|
243
|
+
*
|
|
244
|
+
* ```tsx
|
|
245
|
+
* meta: {
|
|
246
|
+
* exportValue: ({ value }) => STATUS_LABELS[value as Status],
|
|
247
|
+
* }
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
exportValue?: TMDataGridExportValueGetter;
|
|
202
251
|
};
|
|
203
252
|
|
|
204
253
|
/** Grid-wide configuration passed through `options.meta`. */
|
|
@@ -231,6 +280,7 @@ export const tmDataGridFeatures = tableFeatures({
|
|
|
231
280
|
columnResizingFeature,
|
|
232
281
|
columnFacetingFeature,
|
|
233
282
|
columnGroupingFeature,
|
|
283
|
+
rowAggregationFeature,
|
|
234
284
|
// Registered for grouping's sake rather than for tree data: the grouped row
|
|
235
285
|
// model builds the parent rows, and this is what flattens the expanded ones
|
|
236
286
|
// back into the flat list the body virtualizes.
|
|
@@ -270,8 +320,57 @@ export type TMDataGridTable<TData extends RowData> = Table<
|
|
|
270
320
|
TData
|
|
271
321
|
>;
|
|
272
322
|
|
|
273
|
-
|
|
274
|
-
|
|
323
|
+
/** A column definition with `meta` typed against the row type. */
|
|
324
|
+
type WithRowTypedMeta<TDef, TData extends RowData> = TDef extends unknown
|
|
325
|
+
? Omit<TDef, "meta"> & { meta?: TMDataGridColumnMeta<TData> }
|
|
326
|
+
: never;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* TanStack's column helper with `meta` typed against `TData`, so the
|
|
330
|
+
* `meta.options` and `meta.edit.enabled` callbacks receive
|
|
331
|
+
* `Row<TMDataGridFeatures, TData>`.
|
|
332
|
+
*/
|
|
333
|
+
export type TMDataGridColumnHelper<TData extends RowData> = {
|
|
334
|
+
accessor: <
|
|
335
|
+
TAccessor extends AccessorFn<TData> | DeepKeys<TData>,
|
|
336
|
+
TValue extends TAccessor extends AccessorFn<TData, infer TReturn>
|
|
337
|
+
? TReturn
|
|
338
|
+
: TAccessor extends DeepKeys<TData>
|
|
339
|
+
? DeepValue<TData, TAccessor>
|
|
340
|
+
: never,
|
|
341
|
+
>(
|
|
342
|
+
accessor: TAccessor,
|
|
343
|
+
column: WithRowTypedMeta<
|
|
344
|
+
TAccessor extends AccessorFn<TData>
|
|
345
|
+
? DisplayColumnDef<TMDataGridFeatures, TData, TValue>
|
|
346
|
+
: IdentifiedColumnDef<TMDataGridFeatures, TData, TValue>,
|
|
347
|
+
TData
|
|
348
|
+
>,
|
|
349
|
+
) => TAccessor extends AccessorFn<TData>
|
|
350
|
+
? AccessorFnColumnDef<TMDataGridFeatures, TData, TValue>
|
|
351
|
+
: AccessorKeyColumnDef<TMDataGridFeatures, TData, TValue>;
|
|
352
|
+
columns: ColumnHelper<TMDataGridFeatures, TData>["columns"];
|
|
353
|
+
display: (
|
|
354
|
+
column: WithRowTypedMeta<DisplayColumnDef<TMDataGridFeatures, TData>, TData>,
|
|
355
|
+
) => DisplayColumnDef<TMDataGridFeatures, TData, unknown>;
|
|
356
|
+
group: (
|
|
357
|
+
column: WithRowTypedMeta<
|
|
358
|
+
GroupColumnDef<TMDataGridFeatures, TData, unknown>,
|
|
359
|
+
TData
|
|
360
|
+
>,
|
|
361
|
+
) => GroupColumnDef<TMDataGridFeatures, TData, unknown>;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
export function createTMDataGridColumnHelper<
|
|
365
|
+
TData extends RowData,
|
|
366
|
+
>(): TMDataGridColumnHelper<TData> {
|
|
367
|
+
// Same runtime helper. The features object registers one meta type for
|
|
368
|
+
// every table, so the row-typed meta exists only in this signature; the
|
|
369
|
+
// grid calls the callbacks with the rows of the table they belong to.
|
|
370
|
+
return createColumnHelper<
|
|
371
|
+
TMDataGridFeatures,
|
|
372
|
+
TData
|
|
373
|
+
>() as unknown as TMDataGridColumnHelper<TData>;
|
|
275
374
|
}
|
|
276
375
|
|
|
277
376
|
/** What the `renderDetails` render prop is handed for an expanded row. */
|
|
@@ -294,6 +393,9 @@ const DEFAULT_DETAILS_EST_HEIGHT = 160;
|
|
|
294
393
|
/** Rows kept mounted on each side of the viewport. See `overscan`. */
|
|
295
394
|
const DEFAULT_OVERSCAN = 6;
|
|
296
395
|
|
|
396
|
+
/** One empty array, so a selector answering "none" never changes identity. */
|
|
397
|
+
const EMPTY_IDS: ReadonlyArray<string> = [];
|
|
398
|
+
|
|
297
399
|
/**
|
|
298
400
|
* Chrome state that is *not* table state: which panels are open, and which
|
|
299
401
|
* column opened the filter panel. Kept in a TanStack Store so consumers can
|
|
@@ -301,9 +403,20 @@ const DEFAULT_OVERSCAN = 6;
|
|
|
301
403
|
*/
|
|
302
404
|
export type TMDataGridUiState = {
|
|
303
405
|
filterPanelOpen: boolean;
|
|
304
|
-
|
|
305
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Column whose *panel* row should take the focus. Cleared once the row has
|
|
408
|
+
* taken it, so pointing at the same column twice focuses twice.
|
|
409
|
+
*/
|
|
306
410
|
filterPanelColumnId: string | null;
|
|
411
|
+
/**
|
|
412
|
+
* Column whose *header filter* control should take the focus, under
|
|
413
|
+
* `filters.inHeader`. Cleared once taken, like the one above.
|
|
414
|
+
*
|
|
415
|
+
* Its own slot rather than a second reader of `filterPanelColumnId`: a grid
|
|
416
|
+
* can have header filters and a panel at once, and two controls racing to
|
|
417
|
+
* answer one id means whichever mounted last wins the caret.
|
|
418
|
+
*/
|
|
419
|
+
headerFilterColumnId: string | null;
|
|
307
420
|
/**
|
|
308
421
|
* Column being dragged by its header, if any. Held here rather than read from
|
|
309
422
|
* `dataTransfer`, which browsers keep unreadable until the drop.
|
|
@@ -342,13 +455,33 @@ export type TMDataGridUiState = {
|
|
|
342
455
|
* describe different places.
|
|
343
456
|
*/
|
|
344
457
|
cellRange: TMDataGridCellRange | null;
|
|
458
|
+
/**
|
|
459
|
+
* The export column picker, while it is open: which rows it exports and the
|
|
460
|
+
* options of the item that opened it. `null` while closed. Held here rather
|
|
461
|
+
* than in the menu item, which unmounts with the dropdown the moment it is
|
|
462
|
+
* clicked.
|
|
463
|
+
*/
|
|
464
|
+
exportPicker: TMDataGridExportPickerRequest | null;
|
|
345
465
|
};
|
|
346
466
|
|
|
347
467
|
export type TMDataGridUiActions = {
|
|
348
468
|
openFilterPanel: (columnId?: string | null) => void;
|
|
349
469
|
closeFilterPanel: () => void;
|
|
350
|
-
|
|
351
|
-
|
|
470
|
+
/** Opens the export column picker for `request`. See `TMDataGrid.Menu.Export`'s `columns="custom"`. */
|
|
471
|
+
openExportPicker: (request: TMDataGridExportPickerRequest) => void;
|
|
472
|
+
closeExportPicker: () => void;
|
|
473
|
+
/**
|
|
474
|
+
* Points at a column's row in the filter panel without opening anything.
|
|
475
|
+
* `openFilterPanel` does this as well as opening; this is the half a panel
|
|
476
|
+
* that is already showing needs.
|
|
477
|
+
*/
|
|
478
|
+
focusPanelFilter: (columnId: string | null) => void;
|
|
479
|
+
/**
|
|
480
|
+
* Points at a column's header filter control - what `openColumnFilter` does
|
|
481
|
+
* under `filters.inHeader`, where there is no panel to open. The header row
|
|
482
|
+
* scrolls the column into view and focuses it.
|
|
483
|
+
*/
|
|
484
|
+
focusHeaderFilter: (columnId: string | null) => void;
|
|
352
485
|
startColumnDrag: (columnId: string) => void;
|
|
353
486
|
endColumnDrag: () => void;
|
|
354
487
|
/**
|
|
@@ -393,13 +526,28 @@ export type TMDataGridApi<TData extends RowData> = {
|
|
|
393
526
|
ui: TMDataGridUiStore;
|
|
394
527
|
/**
|
|
395
528
|
* The edit engine - open forms, dirty/error projections, and the verbs
|
|
396
|
-
* (`begin`, `commit`, `cancel`, `
|
|
397
|
-
* out the same TanStack Form the inline editors write through
|
|
398
|
-
* or detail panel can share a row's draft
|
|
529
|
+
* (`begin`, `commit`, `cancel`, `saveDrafts`). `edit.getForm(rowId)` hands
|
|
530
|
+
* out the same TanStack Form the inline editors write through while a row
|
|
531
|
+
* is open, so a drawer or detail panel can share a row's draft; a
|
|
532
|
+
* committed row has no form until `begin` reopens it. Inert until
|
|
533
|
+
* `editing` is set.
|
|
399
534
|
*/
|
|
400
535
|
edit: TMDataGridEditApi<TData>;
|
|
401
536
|
/** Table-level feature switches, re-read from options on every render. */
|
|
402
537
|
features: TMDataGridFeatureFlags;
|
|
538
|
+
/**
|
|
539
|
+
* Where the filter controls live, the `filters` option with its defaults
|
|
540
|
+
* filled in. On the api rather than in a component's props because the
|
|
541
|
+
* pills, the column menu and `openColumnFilter` all have to agree with the
|
|
542
|
+
* table about which surface is on.
|
|
543
|
+
*/
|
|
544
|
+
filters: TMDataGridFiltersSettings;
|
|
545
|
+
/**
|
|
546
|
+
* How the grid exports, the `exportOptions` option with its defaults filled
|
|
547
|
+
* in. Read by `useTMDataGridExport`, the `TMDataGrid.Menu.Export*` items and
|
|
548
|
+
* the cell-range menu, so every export of the grid agrees on the format.
|
|
549
|
+
*/
|
|
550
|
+
exportOptions: TMDataGridExportSettings;
|
|
403
551
|
/** Every string the chrome renders, `labels` merged over the English defaults. */
|
|
404
552
|
labels: TMDataGridLabels;
|
|
405
553
|
/** The detail renderer, when row details are on. See `renderDetails`. */
|
|
@@ -588,31 +736,21 @@ export type TMDataGridEditingOptions<TData extends RowData> =
|
|
|
588
736
|
*
|
|
589
737
|
* Rows still open are not in the payload and stay open. Returning
|
|
590
738
|
* nothing saves the whole store and throwing saves none of it;
|
|
591
|
-
* return a {@link
|
|
739
|
+
* return a {@link TMDataGridSaveDraftsResponse} to save part of it.
|
|
592
740
|
*/
|
|
593
741
|
onSaveDrafts?: (
|
|
594
742
|
args: TMDataGridSaveDraftsArgs<TData>,
|
|
595
743
|
) =>
|
|
596
744
|
| void
|
|
597
|
-
|
|
|
598
|
-
| Promise<void |
|
|
599
|
-
/**
|
|
600
|
-
* @deprecated Renamed to {@link onSaveDrafts} - it fires when the
|
|
601
|
-
* draft store is saved, not when a row commits into it. Still
|
|
602
|
-
* honoured; removed in a later beta.
|
|
603
|
-
*/
|
|
604
|
-
onCommitDrafts?: (
|
|
605
|
-
args: TMDataGridSaveDraftsArgs<TData>,
|
|
606
|
-
) =>
|
|
607
|
-
| void
|
|
608
|
-
| TMDataGridSaveDraftsResult
|
|
609
|
-
| Promise<void | TMDataGridSaveDraftsResult>;
|
|
745
|
+
| TMDataGridSaveDraftsResponse
|
|
746
|
+
| Promise<void | TMDataGridSaveDraftsResponse>;
|
|
610
747
|
/**
|
|
611
|
-
* Keep
|
|
612
|
-
* the draft store is saved. Off by default:
|
|
613
|
-
*
|
|
614
|
-
* *typed* into is always sticky, but
|
|
615
|
-
* entering many cannot fill the viewport
|
|
748
|
+
* Keep committed entry rows pinned in the sticky entry block until
|
|
749
|
+
* the draft store is saved, out of the body's sort. Off by default:
|
|
750
|
+
* a committed row joins the body rows instead, sorted and filtered
|
|
751
|
+
* with them - the block a row is *typed* into is always sticky, but
|
|
752
|
+
* committed rows scroll, so entering many cannot fill the viewport
|
|
753
|
+
* with sticky chrome.
|
|
616
754
|
*/
|
|
617
755
|
newRowsSticky?: boolean;
|
|
618
756
|
}
|
|
@@ -621,8 +759,6 @@ export type TMDataGridEditingOptions<TData extends RowData> =
|
|
|
621
759
|
draft?: false;
|
|
622
760
|
/** Only `draft: true` has a store to save - see the other branch. */
|
|
623
761
|
onSaveDrafts?: never;
|
|
624
|
-
/** @deprecated See {@link onSaveDrafts}. */
|
|
625
|
-
onCommitDrafts?: never;
|
|
626
762
|
/** Parked entry rows exist only under `draft: true` - see there. */
|
|
627
763
|
newRowsSticky?: never;
|
|
628
764
|
}
|
|
@@ -688,12 +824,55 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
688
824
|
* no extra flag.
|
|
689
825
|
*/
|
|
690
826
|
enablePagination?: boolean;
|
|
827
|
+
/**
|
|
828
|
+
* Sends the grid back to the first page whenever the query changes - a
|
|
829
|
+
* column filter, the quick search, the sort or the grouping. On by
|
|
830
|
+
* default. TanStack's own `autoResetPageIndex` is switched off by the
|
|
831
|
+
* grid: it fires on any change to the `data` array, which under
|
|
832
|
+
* `editing.draft` is every commit.
|
|
833
|
+
*
|
|
834
|
+
* Server-side, `pageIndex` is a position in a result set the grid does not
|
|
835
|
+
* own: narrowing the query leaves it pointing past the last page, and the
|
|
836
|
+
* next request comes back empty. The reset is applied in the same event as
|
|
837
|
+
* the change, so one request goes out, for the first page of the new query.
|
|
838
|
+
*/
|
|
839
|
+
resetPageOnQueryChange?: boolean;
|
|
691
840
|
/**
|
|
692
841
|
* The row-number gutter: a generated lane, outermost left, numbering the
|
|
693
842
|
* rows of the current view - sorted, filtered, continuing across pages,
|
|
694
843
|
* with group rows unnumbered. Off by default.
|
|
695
844
|
*/
|
|
696
845
|
enableRowNumbers?: boolean;
|
|
846
|
+
/**
|
|
847
|
+
* Where the grid puts its filter controls - a popup over the rows, a sidebar
|
|
848
|
+
* beside them, controls in the header row, or nowhere at all so you place
|
|
849
|
+
* `TMDataGrid.FilterPanel` yourself.
|
|
850
|
+
*
|
|
851
|
+
* ```tsx
|
|
852
|
+
* useTMDataGrid({ data, columns, filters: { surface: "sidebar", inHeader: true } });
|
|
853
|
+
* ```
|
|
854
|
+
*
|
|
855
|
+
* Defaults to `{ surface: "popup" }` - the floating panel the grid has
|
|
856
|
+
* always shown. See {@link TMDataGridFiltersOptions}.
|
|
857
|
+
*
|
|
858
|
+
* Read field by field, so a literal is fine here - unlike `labels` or
|
|
859
|
+
* `persist`, this one does not have to be referentially stable.
|
|
860
|
+
*/
|
|
861
|
+
filters?: TMDataGridFiltersOptions;
|
|
862
|
+
/**
|
|
863
|
+
* How the grid exports: the file format, the file name and whether the
|
|
864
|
+
* column labels go in as the first row. Defaults to `csvExcelFormat()`,
|
|
865
|
+
* `"export"` and `true`. See {@link TMDataGridExportOptions}.
|
|
866
|
+
*
|
|
867
|
+
* ```tsx
|
|
868
|
+
* useTMDataGrid({ data, columns, exportOptions: { format: csvFormat(), fileName: "employees" } });
|
|
869
|
+
* ```
|
|
870
|
+
*
|
|
871
|
+
* Read field by field like `filters`, so a literal is fine. A `format`
|
|
872
|
+
* built inline is rebuilt every render, which costs nothing but a small
|
|
873
|
+
* object; keep it at module scope when that bothers you.
|
|
874
|
+
*/
|
|
875
|
+
exportOptions?: TMDataGridExportOptions;
|
|
697
876
|
/**
|
|
698
877
|
* How the quick search (`TMDataGrid.Search`) matches. `"fuzzy"` - the
|
|
699
878
|
* default - forgives typos and skipped characters, and while it is the
|
|
@@ -957,6 +1136,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
957
1136
|
labels: labelsOverride,
|
|
958
1137
|
enableColumnOrdering,
|
|
959
1138
|
enablePagination,
|
|
1139
|
+
resetPageOnQueryChange,
|
|
960
1140
|
enableRowNumbers,
|
|
961
1141
|
selectionMode,
|
|
962
1142
|
showSelectedBackground,
|
|
@@ -964,6 +1144,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
964
1144
|
onHighlightedRowChange,
|
|
965
1145
|
cellSelection,
|
|
966
1146
|
onFocusedCellChange,
|
|
1147
|
+
filters: filterOptions,
|
|
1148
|
+
exportOptions: exportOptionsOverride,
|
|
967
1149
|
editing,
|
|
968
1150
|
renderDetails,
|
|
969
1151
|
renderDetailsEstHeight = DEFAULT_DETAILS_EST_HEIGHT,
|
|
@@ -995,6 +1177,50 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
995
1177
|
// Resolved on the override's identity, so a module-scope dictionary costs one
|
|
996
1178
|
// merge for the lifetime of the grid.
|
|
997
1179
|
const labels = useMemo(() => mergeLabels(labelsOverride), [labelsOverride]);
|
|
1180
|
+
// Field by field for the same reason as `filters` below.
|
|
1181
|
+
const {
|
|
1182
|
+
format: exportFormat,
|
|
1183
|
+
fileName: exportFileName,
|
|
1184
|
+
includeHeaders: exportIncludeHeaders,
|
|
1185
|
+
columns: exportColumns,
|
|
1186
|
+
} = exportOptionsOverride ?? {};
|
|
1187
|
+
const exportOptions = useMemo(
|
|
1188
|
+
() =>
|
|
1189
|
+
resolveExportOptions({
|
|
1190
|
+
format: exportFormat,
|
|
1191
|
+
fileName: exportFileName,
|
|
1192
|
+
includeHeaders: exportIncludeHeaders,
|
|
1193
|
+
columns: exportColumns,
|
|
1194
|
+
}),
|
|
1195
|
+
[exportFormat, exportFileName, exportIncludeHeaders, exportColumns],
|
|
1196
|
+
);
|
|
1197
|
+
// Unpacked before the memo, so the api is keyed on the five fields rather
|
|
1198
|
+
// than on the object's identity - which is what lets `filters` be written as
|
|
1199
|
+
// a literal, the way it reads best, without republishing every render.
|
|
1200
|
+
const {
|
|
1201
|
+
surface: filterSurface,
|
|
1202
|
+
sidebarSide: filterSidebarSide,
|
|
1203
|
+
sidebarWidth: filterSidebarWidth,
|
|
1204
|
+
defaultOpen: filtersDefaultOpen,
|
|
1205
|
+
inHeader: filtersInHeader,
|
|
1206
|
+
} = filterOptions ?? {};
|
|
1207
|
+
const filters = useMemo(
|
|
1208
|
+
() =>
|
|
1209
|
+
resolveFilterOptions({
|
|
1210
|
+
surface: filterSurface,
|
|
1211
|
+
sidebarSide: filterSidebarSide,
|
|
1212
|
+
sidebarWidth: filterSidebarWidth,
|
|
1213
|
+
defaultOpen: filtersDefaultOpen,
|
|
1214
|
+
inHeader: filtersInHeader,
|
|
1215
|
+
}),
|
|
1216
|
+
[
|
|
1217
|
+
filterSurface,
|
|
1218
|
+
filterSidebarSide,
|
|
1219
|
+
filterSidebarWidth,
|
|
1220
|
+
filtersDefaultOpen,
|
|
1221
|
+
filtersInHeader,
|
|
1222
|
+
],
|
|
1223
|
+
);
|
|
998
1224
|
|
|
999
1225
|
const pinningEnabled = options.enableColumnPinning !== false;
|
|
1000
1226
|
const selectColumnEnabled = features.selectColumn;
|
|
@@ -1169,12 +1395,129 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1169
1395
|
[],
|
|
1170
1396
|
);
|
|
1171
1397
|
|
|
1398
|
+
// The edit engine. Built once per mount; everything it needs later - the
|
|
1399
|
+
// table, the mode, the consumer's callbacks - is read through a ref updated
|
|
1400
|
+
// every render, so forms created at `begin()` always call the latest
|
|
1401
|
+
// `onEditCommit` (the onHighlightedRowChangeRef pattern, applied wholesale).
|
|
1402
|
+
// Created ahead of the table because the table's `data` reads its store;
|
|
1403
|
+
// the ref is filled in once the table exists, and the engine only reads it
|
|
1404
|
+
// inside verbs, never while being built.
|
|
1405
|
+
const editContextRef = useRef<TMDataGridEditEngineContext>(null as never);
|
|
1406
|
+
// The engine is erased; the row type comes back on the way out, which is
|
|
1407
|
+
// what makes `edit.addRow(values)` check against `TData`.
|
|
1408
|
+
const [engine] = useState(() =>
|
|
1409
|
+
createEditEngine(() => editContextRef.current),
|
|
1410
|
+
);
|
|
1411
|
+
const edit = engine as unknown as TMDataGridEditApi<TData>;
|
|
1412
|
+
|
|
1413
|
+
// The rows as shown. Under `editing.draft` a committed row is a row like any
|
|
1414
|
+
// other to the table: its draft replaces the consumer's record and a
|
|
1415
|
+
// committed entry row is prepended, so sorting, filtering, grouping and
|
|
1416
|
+
// aggregates all read the draft store's values. Rows still being typed
|
|
1417
|
+
// into are not here - their values are undecided, and re-sorting under a
|
|
1418
|
+
// caret is not something anyone wants. `newRowsSticky` keeps committed
|
|
1419
|
+
// entry rows in the entry block, so those stay out too.
|
|
1420
|
+
//
|
|
1421
|
+
// Everything is keyed on identities the engine keeps stable while nothing
|
|
1422
|
+
// is decided, so a keystroke in an open editor never rebuilds the model.
|
|
1423
|
+
// `committedValues` rather than `committedRowIds`: the snapshot outlives a
|
|
1424
|
+
// reopen, so a parked row being edited again keeps its place until the
|
|
1425
|
+
// next commit or a cancel decides otherwise.
|
|
1426
|
+
const editCommittedValues = useSelector(
|
|
1427
|
+
edit.store,
|
|
1428
|
+
(state) => state.committedValues,
|
|
1429
|
+
);
|
|
1430
|
+
const newRowsSticky = features.editNewRowsSticky;
|
|
1431
|
+
const editCreatedIds = useSelector(
|
|
1432
|
+
edit.store,
|
|
1433
|
+
(state) =>
|
|
1434
|
+
newRowsSticky
|
|
1435
|
+
? EMPTY_IDS
|
|
1436
|
+
: state.newRows
|
|
1437
|
+
.filter((newRow) => newRow.committed)
|
|
1438
|
+
.map((newRow) => newRow.tempId),
|
|
1439
|
+
{ compare: shallow },
|
|
1440
|
+
);
|
|
1441
|
+
// Read through a ref, not listed as a dependency: an inline `getRowId` is
|
|
1442
|
+
// a fresh function every render, and the merged array must keep its
|
|
1443
|
+
// identity across renders or the table rebuilds its row models on each.
|
|
1444
|
+
const consumerGetRowIdRef = useRef(options.getRowId);
|
|
1445
|
+
consumerGetRowIdRef.current = options.getRowId;
|
|
1446
|
+
const shown = useMemo(() => {
|
|
1447
|
+
const consumerGetRowId = consumerGetRowIdRef.current;
|
|
1448
|
+
const idOf = new Map<object, string>();
|
|
1449
|
+
const created: Array<TData> = [];
|
|
1450
|
+
for (const tempId of editCreatedIds) {
|
|
1451
|
+
const values = editCommittedValues[tempId];
|
|
1452
|
+
if (values === undefined) continue;
|
|
1453
|
+
idOf.set(values, tempId);
|
|
1454
|
+
created.push(values as TData);
|
|
1455
|
+
}
|
|
1456
|
+
const snapshots = Object.keys(editCommittedValues).length;
|
|
1457
|
+
if (snapshots === 0) return { rows: options.data, idOf };
|
|
1458
|
+
// Every snapshot may stand in for a record; only the created ones are
|
|
1459
|
+
// known not to. The rest are looked up by the record's own id.
|
|
1460
|
+
const rows =
|
|
1461
|
+
snapshots === created.length
|
|
1462
|
+
? options.data
|
|
1463
|
+
: options.data.map((record, index) => {
|
|
1464
|
+
const rowId =
|
|
1465
|
+
consumerGetRowId?.(record, index, undefined) ?? String(index);
|
|
1466
|
+
const values = editCommittedValues[rowId];
|
|
1467
|
+
if (values === undefined) return record;
|
|
1468
|
+
idOf.set(values, rowId);
|
|
1469
|
+
return values as TData;
|
|
1470
|
+
});
|
|
1471
|
+
return {
|
|
1472
|
+
rows: created.length === 0 ? rows : [...created, ...rows],
|
|
1473
|
+
idOf,
|
|
1474
|
+
};
|
|
1475
|
+
}, [options.data, editCommittedValues, editCreatedIds]);
|
|
1476
|
+
const shownIdOfRef = useRef(shown.idOf);
|
|
1477
|
+
shownIdOfRef.current = shown.idOf;
|
|
1478
|
+
// A stable id resolver over the merged rows. A draft answers the id of the
|
|
1479
|
+
// record it stands in for - the consumer's `getRowId` never sees a draft
|
|
1480
|
+
// object, so an editable id field cannot rename the row - and a created
|
|
1481
|
+
// row answers its temp id. Everything else is the consumer's own, or
|
|
1482
|
+
// TanStack's index fallback.
|
|
1483
|
+
const shownGetRowId = useCallback(
|
|
1484
|
+
(record: TData, index: number, parent?: Row<TMDataGridFeatures, TData>) =>
|
|
1485
|
+
shownIdOfRef.current.get(record) ??
|
|
1486
|
+
consumerGetRowIdRef.current?.(record, index, parent) ??
|
|
1487
|
+
(parent !== undefined ? `${parent.id}.${index}` : String(index)),
|
|
1488
|
+
[],
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
// Filled immediately after the call below. The query-change wrappers close
|
|
1492
|
+
// over it rather than over `table`, since they are built as part of the
|
|
1493
|
+
// options the table is constructed from.
|
|
1494
|
+
const tableRef = useRef<TMDataGridTable<TData>>(null as never);
|
|
1495
|
+
// A narrower query invalidates the page the grid is on - see pageReset.ts.
|
|
1496
|
+
// On by default everywhere: TanStack's own `autoResetPageIndex` is switched
|
|
1497
|
+
// off below, since it also fires on every draft commit.
|
|
1498
|
+
const resetPage = resetPageOnQueryChange ?? true;
|
|
1499
|
+
const getQueryTable = useCallback(
|
|
1500
|
+
() => tableRef.current as unknown as TMDataGridQueryTable,
|
|
1501
|
+
[],
|
|
1502
|
+
);
|
|
1503
|
+
|
|
1504
|
+
// The sync of `state` into the table's atoms happens inside this call, in
|
|
1505
|
+
// the render body, and publishing from there makes React warn about the
|
|
1506
|
+
// consumer's component - see controlledStateSync.ts.
|
|
1507
|
+
beginControlledStateSync();
|
|
1172
1508
|
const table = useTable({
|
|
1173
1509
|
// The grid paints a running drag itself, one style write per frame - see
|
|
1174
1510
|
// the resize preview in TMDataGridTable - and takes the width into state
|
|
1175
1511
|
// once, when the pointer is released. `"onChange"` publishes a width on
|
|
1176
1512
|
// every pointer move instead, which re-renders the grid for each of them.
|
|
1177
1513
|
columnResizeMode: "onEnd",
|
|
1514
|
+
// TanStack resets both whenever the `data` array's identity changes, and
|
|
1515
|
+
// the rows as shown are a new array on every draft commit (see `shown`),
|
|
1516
|
+
// so a commit on page 3 would land on page 1 with every details panel
|
|
1517
|
+
// closed. Off here; the page reset the grid does want - on a query
|
|
1518
|
+
// change - is its own, below. A consumer's explicit option still wins.
|
|
1519
|
+
autoResetExpanded: false,
|
|
1520
|
+
autoResetPageIndex: false,
|
|
1178
1521
|
enableSorting: true,
|
|
1179
1522
|
enableColumnResizing: true,
|
|
1180
1523
|
// The quick search's matcher. Fuzzy by default (Q4); `"contains"` keeps
|
|
@@ -1191,6 +1534,41 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1191
1534
|
// `"reorder"` to keep the column and have it moved to the front instead.
|
|
1192
1535
|
groupedColumnMode: "remove",
|
|
1193
1536
|
...options,
|
|
1537
|
+
// The query slices, wrapped so a change also takes the grid back to the
|
|
1538
|
+
// first page - see pageReset.ts. Spread conditionally: the keys carry a
|
|
1539
|
+
// `makeStateUpdater` default, and an explicit `undefined` would overwrite
|
|
1540
|
+
// it and leave the slice unwritable.
|
|
1541
|
+
...(resetPage
|
|
1542
|
+
? {
|
|
1543
|
+
onColumnFiltersChange: withPageReset(
|
|
1544
|
+
"columnFilters",
|
|
1545
|
+
options.onColumnFiltersChange as never,
|
|
1546
|
+
getQueryTable,
|
|
1547
|
+
) as TableOptions<
|
|
1548
|
+
TMDataGridFeatures,
|
|
1549
|
+
TData
|
|
1550
|
+
>["onColumnFiltersChange"],
|
|
1551
|
+
onGlobalFilterChange: withPageReset(
|
|
1552
|
+
"globalFilter",
|
|
1553
|
+
options.onGlobalFilterChange as never,
|
|
1554
|
+
getQueryTable,
|
|
1555
|
+
) as TableOptions<TMDataGridFeatures, TData>["onGlobalFilterChange"],
|
|
1556
|
+
onSortingChange: withPageReset(
|
|
1557
|
+
"sorting",
|
|
1558
|
+
options.onSortingChange as never,
|
|
1559
|
+
getQueryTable,
|
|
1560
|
+
) as TableOptions<TMDataGridFeatures, TData>["onSortingChange"],
|
|
1561
|
+
onGroupingChange: withPageReset(
|
|
1562
|
+
"grouping",
|
|
1563
|
+
options.onGroupingChange as never,
|
|
1564
|
+
getQueryTable,
|
|
1565
|
+
) as TableOptions<TMDataGridFeatures, TData>["onGroupingChange"],
|
|
1566
|
+
}
|
|
1567
|
+
: {}),
|
|
1568
|
+
// The rows as shown - see `shown` above. The consumer's own array passes
|
|
1569
|
+
// through untouched while nothing is committed.
|
|
1570
|
+
data: shown.rows,
|
|
1571
|
+
getRowId: shownGetRowId,
|
|
1194
1572
|
// Row details ride on `expanded`, the same state the tree uses - but a data
|
|
1195
1573
|
// row answers `getCanExpand()` false, since TanStack's fallback is
|
|
1196
1574
|
// `subRows.length > 0`. `() => true` is the right answer for a group row
|
|
@@ -1209,6 +1587,20 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1209
1587
|
(typeof options.enableRowPinning === "function"
|
|
1210
1588
|
? options.enableRowPinning(row)
|
|
1211
1589
|
: options.enableRowPinning === true),
|
|
1590
|
+
// A deletion-marked row is not selectable: it is on its way out, and a
|
|
1591
|
+
// bulk action over the selection must not see it. TanStack reads the
|
|
1592
|
+
// predicate on every call, so the mark is checked live against the
|
|
1593
|
+
// engine. Only under `editing.draft`, the one place marks exist; the
|
|
1594
|
+
// consumer's own option keeps the final say, predicate form included.
|
|
1595
|
+
...(editDraft
|
|
1596
|
+
? {
|
|
1597
|
+
enableRowSelection: (row: Row<TMDataGridFeatures, TData>) =>
|
|
1598
|
+
!engine.isRowDeleted(row.id) &&
|
|
1599
|
+
(typeof options.enableRowSelection === "function"
|
|
1600
|
+
? options.enableRowSelection(row)
|
|
1601
|
+
: options.enableRowSelection !== false),
|
|
1602
|
+
}
|
|
1603
|
+
: {}),
|
|
1212
1604
|
features: tmDataGridFeatures,
|
|
1213
1605
|
columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
|
|
1214
1606
|
// The stabilized controlled state; `undefined` when nothing is controlled.
|
|
@@ -1230,14 +1622,14 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1230
1622
|
columnPinning: {
|
|
1231
1623
|
// The generated columns are structurally pinned, so they are re-applied
|
|
1232
1624
|
// on top of anything restored from storage.
|
|
1233
|
-
|
|
1625
|
+
start: [
|
|
1234
1626
|
...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
|
|
1235
1627
|
...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
1236
1628
|
...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
|
|
1237
1629
|
...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
|
|
1238
1630
|
...(
|
|
1239
|
-
persistedState.columnPinning?.
|
|
1240
|
-
options.initialState?.columnPinning?.
|
|
1631
|
+
persistedState.columnPinning?.start ??
|
|
1632
|
+
options.initialState?.columnPinning?.start ??
|
|
1241
1633
|
[]
|
|
1242
1634
|
).filter(
|
|
1243
1635
|
(id) =>
|
|
@@ -1249,10 +1641,10 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1249
1641
|
],
|
|
1250
1642
|
// The edit lane mirrors the generated columns on the left: structurally
|
|
1251
1643
|
// pinned, outermost, re-applied over anything restored.
|
|
1252
|
-
|
|
1644
|
+
end: [
|
|
1253
1645
|
...(
|
|
1254
|
-
persistedState.columnPinning?.
|
|
1255
|
-
options.initialState?.columnPinning?.
|
|
1646
|
+
persistedState.columnPinning?.end ??
|
|
1647
|
+
options.initialState?.columnPinning?.end ??
|
|
1256
1648
|
[]
|
|
1257
1649
|
).filter((id) => id !== EDIT_COLUMN_ID),
|
|
1258
1650
|
...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
|
|
@@ -1266,12 +1658,11 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1266
1658
|
},
|
|
1267
1659
|
},
|
|
1268
1660
|
}, selectSettledState);
|
|
1661
|
+
endControlledStateSync();
|
|
1662
|
+
tableRef.current = table as unknown as TMDataGridTable<TData>;
|
|
1663
|
+
deferControlledStateSyncPublishes(table.store);
|
|
1269
1664
|
|
|
1270
|
-
// The
|
|
1271
|
-
// table, the mode, the consumer's callbacks - is read through a ref updated
|
|
1272
|
-
// every render, so forms created at `begin()` always call the latest
|
|
1273
|
-
// `onEditCommit` (the onHighlightedRowChangeRef pattern, applied wholesale).
|
|
1274
|
-
const editContextRef = useRef<TMDataGridEditEngineContext>(null as never);
|
|
1665
|
+
// The engine's view of this render - see the engine's creation above.
|
|
1275
1666
|
editContextRef.current = {
|
|
1276
1667
|
table: table as unknown as TMDataGridTable<TMDataGridRowData>,
|
|
1277
1668
|
editMode: editMode ?? "cell",
|
|
@@ -1284,23 +1675,14 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1284
1675
|
editing?.isRowEditable as TMDataGridEditEngineContext["isRowEditable"],
|
|
1285
1676
|
onEditCommit:
|
|
1286
1677
|
editing?.onCommit as TMDataGridEditEngineContext["onEditCommit"],
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
editing?.onCommitDrafts) as TMDataGridEditEngineContext["onSaveDrafts"],
|
|
1678
|
+
onSaveDrafts:
|
|
1679
|
+
editing?.onSaveDrafts as TMDataGridEditEngineContext["onSaveDrafts"],
|
|
1290
1680
|
newRowDefaults:
|
|
1291
1681
|
editing?.newRowDefaults as TMDataGridEditEngineContext["newRowDefaults"],
|
|
1292
1682
|
onRowAdd: editing?.onRowAdd as TMDataGridEditEngineContext["onRowAdd"],
|
|
1293
1683
|
onRowDelete:
|
|
1294
1684
|
editing?.onRowDelete as TMDataGridEditEngineContext["onRowDelete"],
|
|
1295
1685
|
};
|
|
1296
|
-
// The engine is erased; the row type comes back on the way out, which is
|
|
1297
|
-
// what makes `edit.addRow(values)` check against `TData`.
|
|
1298
|
-
const [edit] = useState(
|
|
1299
|
-
() =>
|
|
1300
|
-
createEditEngine(
|
|
1301
|
-
() => editContextRef.current,
|
|
1302
|
-
) as unknown as TMDataGridEditApi<TData>,
|
|
1303
|
-
);
|
|
1304
1686
|
|
|
1305
1687
|
// Switching either axis mid-flight drops every draft: the policies disagree
|
|
1306
1688
|
// about what an open form means, and carrying one across is how a row
|
|
@@ -1315,6 +1697,29 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1315
1697
|
edit.cancelAll();
|
|
1316
1698
|
}, [editMode, editDraft, edit]);
|
|
1317
1699
|
|
|
1700
|
+
// A refetch that drops a record takes the engine's state for it along -
|
|
1701
|
+
// see forgetMissingRows. Not where the grid does not own the result set:
|
|
1702
|
+
// there a row missing from `data` is on another page or filtered away
|
|
1703
|
+
// server-side, not gone, and its draft has to wait for Save. Keyed on the
|
|
1704
|
+
// rows as shown, which is what the core row model is built from.
|
|
1705
|
+
// Through the ref, not `table`: `useTable` hands out a fresh copy of the
|
|
1706
|
+
// table every render, and listing it would run this on every one.
|
|
1707
|
+
const editingOn = editing !== undefined;
|
|
1708
|
+
const serverSideRows =
|
|
1709
|
+
options.manualPagination === true || options.manualFiltering === true;
|
|
1710
|
+
useEffect(() => {
|
|
1711
|
+
if (!editingOn || serverSideRows) return;
|
|
1712
|
+
engine.forgetMissingRows(tableRef.current.getCoreRowModel().rowsById);
|
|
1713
|
+
}, [shown.rows, editingOn, serverSideRows, engine]);
|
|
1714
|
+
|
|
1715
|
+
// Readers that hold the table and nothing else - `exportGrid` - still
|
|
1716
|
+
// leave a deletion-marked row out. Keyed on
|
|
1717
|
+
// the store, which every copy of the table shares, so once is enough.
|
|
1718
|
+
useEffect(
|
|
1719
|
+
() => registerDeletedRows(tableRef.current, engine.isRowDeleted),
|
|
1720
|
+
[engine],
|
|
1721
|
+
);
|
|
1722
|
+
|
|
1318
1723
|
// Editing without stable ids points every draft at whatever record slides
|
|
1319
1724
|
// into that index after a sort. Loud, once, in development.
|
|
1320
1725
|
useEffect(() => {
|
|
@@ -1341,42 +1746,16 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1341
1746
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1342
1747
|
}, []);
|
|
1343
1748
|
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
// One: the tree column appears with the first grouped column and goes away
|
|
1347
|
-
// with the last, so an ungrouped grid looks exactly as it did before grouping
|
|
1749
|
+
// The tree column appears with the first grouped column and goes away with
|
|
1750
|
+
// the last, so an ungrouped grid looks exactly as it did before grouping
|
|
1348
1751
|
// existed. Driven from a subscription rather than by rebuilding the column
|
|
1349
1752
|
// array, because the array is what the table is built from - deriving it from
|
|
1350
1753
|
// table state would close the loop. Visibility is the one column property
|
|
1351
1754
|
// that can be changed after the fact without touching the definitions.
|
|
1352
1755
|
//
|
|
1353
|
-
// Two, and this one is a workaround. In table-core 9.0.0-beta.21 the
|
|
1354
|
-
// per-region column APIs do not list `grouping` among their memo
|
|
1355
|
-
// dependencies, even though they all derive from `getAllLeafColumns()`, which
|
|
1356
|
-
// does:
|
|
1357
|
-
//
|
|
1358
|
-
// | API | Declares |
|
|
1359
|
-
// | --- | --- |
|
|
1360
|
-
// | `getLeft/Center/RightVisibleLeafColumns` | columns, columnPinning, columnVisibility, columnOrder |
|
|
1361
|
-
// | `getLeft/Center/RightHeaderGroups` | columnPinning, columnOrder |
|
|
1362
|
-
// | `row.getLeft/Center/RightVisibleCells` | columnPinning, columnVisibility |
|
|
1363
|
-
//
|
|
1364
|
-
// So grouping a *second* column leaves every one of them returning the
|
|
1365
|
-
// previous list: the column TanStack removed keeps its header and its grid
|
|
1366
|
-
// track, and the row cells no longer line up with them. The first grouping
|
|
1367
|
-
// appears to work only because the visibility write above happens to touch a
|
|
1368
|
-
// dependency they share.
|
|
1369
|
-
//
|
|
1370
|
-
// Re-publishing `columnVisibility` and `columnOrder` - same contents, new
|
|
1371
|
-
// identity - invalidates all three families. `columnOrder` is the only
|
|
1372
|
-
// dependency the header groups declare, and `columnVisibility` the only one
|
|
1373
|
-
// the cells do, so both are needed. Remove this once the deps are fixed
|
|
1374
|
-
// upstream; the test that fails without it groups two columns and asserts the
|
|
1375
|
-
// second one leaves the grid.
|
|
1376
|
-
//
|
|
1377
1756
|
// Writing back into the store from its own subscriber is safe: the guard is
|
|
1378
|
-
// on `grouping`'s identity, and
|
|
1379
|
-
//
|
|
1757
|
+
// on `grouping`'s identity, and the write does not touch it, so the callback
|
|
1758
|
+
// it triggers short-circuits.
|
|
1380
1759
|
useEffect(() => {
|
|
1381
1760
|
if (!groupColumnEnabled) return;
|
|
1382
1761
|
let previousGrouping = table.store.state.grouping;
|
|
@@ -1410,8 +1789,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1410
1789
|
// Keeps the entry injected into a controlled `columnVisibility` (see
|
|
1411
1790
|
// requestedState) in sync with grouping.
|
|
1412
1791
|
groupingActiveRef.current = state.grouping.length > 0;
|
|
1413
|
-
// On a controlled slice the
|
|
1414
|
-
// consumer's handler; the next render must forward
|
|
1792
|
+
// On a controlled slice the write below round-trips through the
|
|
1793
|
+
// consumer's handler; the next render must forward it unstabilized.
|
|
1415
1794
|
// See republishControlledStateRef.
|
|
1416
1795
|
republishControlledStateRef.current = true;
|
|
1417
1796
|
|
|
@@ -1419,7 +1798,6 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1419
1798
|
...old,
|
|
1420
1799
|
[GROUP_COLUMN_ID]: state.grouping.length > 0,
|
|
1421
1800
|
}));
|
|
1422
|
-
table.setColumnOrder((old) => [...old]);
|
|
1423
1801
|
});
|
|
1424
1802
|
|
|
1425
1803
|
return () => subscription.unsubscribe();
|
|
@@ -1464,9 +1842,11 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1464
1842
|
|
|
1465
1843
|
const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
|
|
1466
1844
|
{
|
|
1467
|
-
|
|
1468
|
-
|
|
1845
|
+
// `useCreateStore` builds the store once per mount, so `defaultOpen` is
|
|
1846
|
+
// read the way `initialState` is - a starting point, not a controller.
|
|
1847
|
+
filterPanelOpen: filters.defaultOpen,
|
|
1469
1848
|
filterPanelColumnId: null,
|
|
1849
|
+
headerFilterColumnId: null,
|
|
1470
1850
|
draggedColumnId: null,
|
|
1471
1851
|
// `useCreateStore` builds the store once per mount, so this is a genuine
|
|
1472
1852
|
// default rather than a value that would fight later clicks.
|
|
@@ -1474,6 +1854,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1474
1854
|
selectionAnchorRowId: null,
|
|
1475
1855
|
focusedCell: null,
|
|
1476
1856
|
cellRange: null,
|
|
1857
|
+
exportPicker: null,
|
|
1477
1858
|
},
|
|
1478
1859
|
({ setState }) => ({
|
|
1479
1860
|
openFilterPanel: (columnId = null) =>
|
|
@@ -1488,10 +1869,14 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1488
1869
|
filterPanelOpen: false,
|
|
1489
1870
|
filterPanelColumnId: null,
|
|
1490
1871
|
})),
|
|
1491
|
-
|
|
1492
|
-
setState((prev) => ({ ...prev,
|
|
1493
|
-
|
|
1494
|
-
setState((prev) => ({ ...prev,
|
|
1872
|
+
openExportPicker: (request) =>
|
|
1873
|
+
setState((prev) => ({ ...prev, exportPicker: request })),
|
|
1874
|
+
closeExportPicker: () =>
|
|
1875
|
+
setState((prev) => ({ ...prev, exportPicker: null })),
|
|
1876
|
+
focusPanelFilter: (columnId) =>
|
|
1877
|
+
setState((prev) => ({ ...prev, filterPanelColumnId: columnId })),
|
|
1878
|
+
focusHeaderFilter: (columnId) =>
|
|
1879
|
+
setState((prev) => ({ ...prev, headerFilterColumnId: columnId })),
|
|
1495
1880
|
startColumnDrag: (columnId) =>
|
|
1496
1881
|
setState((prev) => ({ ...prev, draggedColumnId: columnId })),
|
|
1497
1882
|
endColumnDrag: () =>
|
|
@@ -1580,12 +1965,12 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1580
1965
|
table.setColumnSizing({ ...initial?.columnSizing });
|
|
1581
1966
|
table.setColumnOrder([...(initial?.columnOrder ?? [])]);
|
|
1582
1967
|
table.setColumnPinning({
|
|
1583
|
-
|
|
1968
|
+
start: [
|
|
1584
1969
|
...(rowNumbersEnabled && pinningEnabled ? [ROW_NUMBER_COLUMN_ID] : []),
|
|
1585
1970
|
...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
1586
1971
|
...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
|
|
1587
1972
|
...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
|
|
1588
|
-
...(initial?.columnPinning?.
|
|
1973
|
+
...(initial?.columnPinning?.start ?? []).filter(
|
|
1589
1974
|
(id) =>
|
|
1590
1975
|
id !== ROW_NUMBER_COLUMN_ID &&
|
|
1591
1976
|
id !== SELECT_COLUMN_ID &&
|
|
@@ -1593,8 +1978,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1593
1978
|
id !== GROUP_COLUMN_ID,
|
|
1594
1979
|
),
|
|
1595
1980
|
],
|
|
1596
|
-
|
|
1597
|
-
...(initial?.columnPinning?.
|
|
1981
|
+
end: [
|
|
1982
|
+
...(initial?.columnPinning?.end ?? []).filter(
|
|
1598
1983
|
(id) => id !== EDIT_COLUMN_ID,
|
|
1599
1984
|
),
|
|
1600
1985
|
...(editColumnEnabled && pinningEnabled ? [EDIT_COLUMN_ID] : []),
|
|
@@ -1617,6 +2002,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1617
2002
|
ui,
|
|
1618
2003
|
edit,
|
|
1619
2004
|
features,
|
|
2005
|
+
filters,
|
|
2006
|
+
exportOptions,
|
|
1620
2007
|
labels,
|
|
1621
2008
|
renderDetails,
|
|
1622
2009
|
renderDetailsEstHeight,
|
|
@@ -1628,20 +2015,44 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1628
2015
|
}
|
|
1629
2016
|
|
|
1630
2017
|
/**
|
|
1631
|
-
*
|
|
1632
|
-
*
|
|
2018
|
+
* Gives a column an empty filter of its default operator, unless it already
|
|
2019
|
+
* has one - which is what makes a surface open on a row rather than on
|
|
2020
|
+
* nothing.
|
|
2021
|
+
*
|
|
2022
|
+
* @internal Shared by `openColumnFilter` and the toolbar's filter button.
|
|
1633
2023
|
*/
|
|
1634
|
-
export function
|
|
2024
|
+
export function seedColumnFilter<TData extends RowData>(
|
|
1635
2025
|
api: TMDataGridApi<TData>,
|
|
1636
2026
|
columnId: string,
|
|
1637
2027
|
): void {
|
|
1638
2028
|
const column = api.table.getColumn(columnId);
|
|
1639
|
-
if (column
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2029
|
+
if (column === undefined || column.getFilterValue() !== undefined) return;
|
|
2030
|
+
const operator = getColumnDefaultOperator(column);
|
|
2031
|
+
column.setFilterValue({ operator, value: emptyValueForOperator(operator) });
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Sends the user to a column's filter control, seeding an empty filter when
|
|
2036
|
+
* the column has none yet - what "Filter" in the column menu and a click on a
|
|
2037
|
+
* filter pill both do.
|
|
2038
|
+
*
|
|
2039
|
+
* Which control that is follows the grid's `filters` option. Under
|
|
2040
|
+
* `inHeader` it is the column's header control, which is already on screen, so
|
|
2041
|
+
* the call focuses it and leaves the popup or sidebar closed. Otherwise it is
|
|
2042
|
+
* the panel's row for that column, and the call opens the surface on it.
|
|
2043
|
+
*/
|
|
2044
|
+
export function openColumnFilter<TData extends RowData>(
|
|
2045
|
+
api: TMDataGridApi<TData>,
|
|
2046
|
+
columnId: string,
|
|
2047
|
+
): void {
|
|
2048
|
+
seedColumnFilter(api, columnId);
|
|
2049
|
+
if (api.filters.inHeader) {
|
|
2050
|
+
// The header control is always visible, so there is nothing to open -
|
|
2051
|
+
// only a column to point at. The header row watching this focuses it and
|
|
2052
|
+
// scrolls it into view. The panel, if one is also showing, is left alone:
|
|
2053
|
+
// it reads the other slot.
|
|
2054
|
+
api.ui.actions.focusHeaderFilter(columnId);
|
|
2055
|
+
return;
|
|
1645
2056
|
}
|
|
1646
2057
|
api.ui.actions.openFilterPanel(columnId);
|
|
1647
2058
|
}
|