@cdx-ui/data-grid 0.3.12 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/coverage.svg +1 -1
  2. package/lib/commonjs/DataGrid.js +279 -40
  3. package/lib/commonjs/DataGrid.js.map +1 -1
  4. package/lib/commonjs/DataGridFilterPanel.js +136 -9
  5. package/lib/commonjs/DataGridFilterPanel.js.map +1 -1
  6. package/lib/commonjs/adapter/columns.js +16 -8
  7. package/lib/commonjs/adapter/columns.js.map +1 -1
  8. package/lib/commonjs/adapter/pinning.js +5 -1
  9. package/lib/commonjs/adapter/pinning.js.map +1 -1
  10. package/lib/commonjs/adapter/selectColumn.js +11 -0
  11. package/lib/commonjs/adapter/selectColumn.js.map +1 -1
  12. package/lib/commonjs/adapter/treeData.js +195 -0
  13. package/lib/commonjs/adapter/treeData.js.map +1 -0
  14. package/lib/commonjs/styles.js +48 -2
  15. package/lib/commonjs/styles.js.map +1 -1
  16. package/lib/commonjs/types.js +11 -1
  17. package/lib/commonjs/types.js.map +1 -1
  18. package/lib/commonjs/useDataGridTable.js +143 -12
  19. package/lib/commonjs/useDataGridTable.js.map +1 -1
  20. package/lib/module/DataGrid.js +285 -46
  21. package/lib/module/DataGrid.js.map +1 -1
  22. package/lib/module/DataGridFilterPanel.js +139 -13
  23. package/lib/module/DataGridFilterPanel.js.map +1 -1
  24. package/lib/module/adapter/columns.js +17 -9
  25. package/lib/module/adapter/columns.js.map +1 -1
  26. package/lib/module/adapter/pinning.js +5 -1
  27. package/lib/module/adapter/pinning.js.map +1 -1
  28. package/lib/module/adapter/selectColumn.js +11 -1
  29. package/lib/module/adapter/selectColumn.js.map +1 -1
  30. package/lib/module/adapter/treeData.js +187 -0
  31. package/lib/module/adapter/treeData.js.map +1 -0
  32. package/lib/module/styles.js +47 -1
  33. package/lib/module/styles.js.map +1 -1
  34. package/lib/module/types.js +10 -0
  35. package/lib/module/types.js.map +1 -1
  36. package/lib/module/useDataGridTable.js +147 -16
  37. package/lib/module/useDataGridTable.js.map +1 -1
  38. package/lib/typescript/DataGrid.d.ts.map +1 -1
  39. package/lib/typescript/DataGridFilterPanel.d.ts +39 -4
  40. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -1
  41. package/lib/typescript/adapter/columns.d.ts +1 -1
  42. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  43. package/lib/typescript/adapter/pinning.d.ts +1 -0
  44. package/lib/typescript/adapter/pinning.d.ts.map +1 -1
  45. package/lib/typescript/adapter/selectColumn.d.ts +8 -0
  46. package/lib/typescript/adapter/selectColumn.d.ts.map +1 -1
  47. package/lib/typescript/adapter/treeData.d.ts +36 -0
  48. package/lib/typescript/adapter/treeData.d.ts.map +1 -0
  49. package/lib/typescript/index.d.ts +1 -1
  50. package/lib/typescript/index.d.ts.map +1 -1
  51. package/lib/typescript/styles.d.ts +12 -1
  52. package/lib/typescript/styles.d.ts.map +1 -1
  53. package/lib/typescript/types.d.ts +68 -0
  54. package/lib/typescript/types.d.ts.map +1 -1
  55. package/lib/typescript/useDataGridTable.d.ts +25 -3
  56. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  57. package/package.json +5 -5
  58. package/src/DataGrid.tsx +399 -59
  59. package/src/DataGridFilterPanel.tsx +188 -17
  60. package/src/adapter/columns.ts +21 -7
  61. package/src/adapter/pinning.ts +10 -1
  62. package/src/adapter/selectColumn.ts +12 -1
  63. package/src/adapter/treeData.ts +228 -0
  64. package/src/index.ts +4 -0
  65. package/src/styles.ts +57 -1
  66. package/src/types.ts +76 -0
  67. package/src/useDataGridTable.ts +217 -15
package/src/styles.ts CHANGED
@@ -180,8 +180,21 @@ export const dataGridBodyVariants = cva(
180
180
 
181
181
  export const dataGridFilterInputVariants = cva('min-w-24');
182
182
 
183
+ // The toolbar panel sits at a fixed spot in the grid root. A panel opened from a column is anchored
184
+ // to its opener in a portal, so its position and width cap come from the anchored overlay instead.
183
185
  export const dataGridFilterPanelVariants = cva(
184
- 'absolute start-4 top-16 z-30 w-max max-w-[calc(100%-2rem)] gap-3 rounded-lg border border-stroke-secondary bg-surface-primary px-3 pb-2 pt-3 shadow-lg',
186
+ 'w-max gap-3 rounded-lg border border-stroke-secondary bg-surface-primary px-3 pb-2 pt-3 shadow-lg',
187
+ {
188
+ variants: {
189
+ anchored: {
190
+ true: '',
191
+ false: 'absolute start-4 top-16 z-30 max-w-[calc(100%-2rem)]',
192
+ },
193
+ },
194
+ defaultVariants: {
195
+ anchored: false,
196
+ },
197
+ },
185
198
  );
186
199
 
187
200
  // The panel is as wide as one unwrapped row, capped at the grid's width. Below that, fields wrap
@@ -234,6 +247,49 @@ export const dataGridCellVariants = cva([
234
247
  }),
235
248
  ]);
236
249
 
250
+ export const dataGridDetailToggleCellVariants = cva([
251
+ 'w-12 px-2',
252
+ Platform.select({
253
+ web: 'border-b border-stroke-secondary',
254
+ default: '',
255
+ }),
256
+ ]);
257
+
258
+ // Header labels and depth-0 content line up because both reserve the toggle slot.
259
+ export const dataGridTreeCellInnerVariants = cva('min-w-0 flex-1 flex-row items-center gap-2');
260
+
261
+ export const dataGridTreeToggleSlotVariants = cva('size-8 shrink-0 items-center justify-center');
262
+
263
+ export const dataGridTreeContentVariants = cva('min-w-0 flex-1 flex-row items-center');
264
+
265
+ export const dataGridDetailPanelCellVariants = cva([
266
+ 'px-4 py-3',
267
+ Platform.select({
268
+ web: 'border-b border-stroke-secondary bg-surface-primary',
269
+ default: '',
270
+ }),
271
+ ]);
272
+
273
+ // Same `w-0 min-w-full` trick as the header label: a declared column width truncates the value
274
+ // with an ellipsis instead of wrapping it onto a second line and growing the fixed-height row.
275
+ export const dataGridCellTextVariants = cva('min-w-0', {
276
+ variants: {
277
+ align: {
278
+ start: 'text-start',
279
+ center: 'text-center',
280
+ end: 'text-end',
281
+ },
282
+ fitToColumn: {
283
+ true: 'w-0 min-w-full',
284
+ false: '',
285
+ },
286
+ },
287
+ defaultVariants: {
288
+ align: 'start',
289
+ fitToColumn: false,
290
+ },
291
+ });
292
+
237
293
  export const dataGridSelectCellVariants = cva([
238
294
  'w-14 px-4',
239
295
  Platform.select({
package/src/types.ts CHANGED
@@ -166,6 +166,31 @@ export type GridFeatureMode = 'client' | 'server';
166
166
 
167
167
  export type GridPaginationMode = GridFeatureMode;
168
168
 
169
+ /** Maps a column `field` to `false` to hide it. Fields that are missing or `true` stay visible. */
170
+ export type GridColumnVisibilityModel = Record<string, boolean>;
171
+
172
+ export interface GridRowParams<R extends GridValidRowModel = GridValidRowModel> {
173
+ id: GridRowId;
174
+ row: R;
175
+ }
176
+
177
+ export interface GridTreeNodeParams<
178
+ R extends GridValidRowModel = GridValidRowModel,
179
+ > extends GridRowParams<R> {
180
+ /** `0` for top-level rows. */
181
+ depth: number;
182
+ }
183
+
184
+ /**
185
+ * How a selection change spreads through `treeData`. Both default to `true`, as in MUI.
186
+ */
187
+ export interface GridRowSelectionPropagation {
188
+ /** Selecting or deselecting a parent applies to its descendants that pass the current filter. */
189
+ descendants?: boolean;
190
+ /** A parent is selected exactly when all of its children are selected. */
191
+ parents?: boolean;
192
+ }
193
+
169
194
  export interface DataGridProps<R extends GridValidRowModel = GridValidRowModel> {
170
195
  rows: GridRowsProp<R>;
171
196
  columns: readonly GridColDef<R>[];
@@ -225,7 +250,53 @@ export interface DataGridProps<R extends GridValidRowModel = GridValidRowModel>
225
250
  * MUI X Community ships no column pinning; this is a Pro feature.
226
251
  */
227
252
  pinnedColumns?: GridPinnedColumnFields;
253
+ /**
254
+ * Hides columns without removing them from `columns`. Hidden columns still sort, filter, and
255
+ * match the quick filter, and the filter panel still lists them. The grid has no built-in
256
+ * control that changes visibility, so keep the model in your own state.
257
+ */
258
+ columnVisibilityModel?: GridColumnVisibilityModel;
228
259
  headerFilters?: boolean;
260
+ /**
261
+ * Renders `rows` as a tree. Each row's position comes from `getTreeDataPath`. The first column
262
+ * becomes the tree column: it indents by depth and holds the expand toggle and, with
263
+ * `checkboxSelection`, the row checkbox. Search and filters keep the ancestors of every match
264
+ * and expand them while the filter is active.
265
+ *
266
+ * Client mode only: server pagination, sorting, and filtering are not supported with tree data.
267
+ * Pagination counts top-level rows, so a group never splits across pages.
268
+ * @default false
269
+ */
270
+ treeData?: boolean;
271
+ /**
272
+ * Path from the root to the row, for example `['Accounts', 'OAuth V1', 'List accounts']`.
273
+ * Paths must be unique. A row whose parent path has no row attaches to its nearest existing
274
+ * ancestor. Keep the function reference stable.
275
+ */
276
+ getTreeDataPath?: (row: R) => readonly string[];
277
+ /**
278
+ * Overrides the tree column (the first column) when `treeData` is on: header name, width,
279
+ * `renderCell`, and so on. `field` stays the first column's field.
280
+ */
281
+ groupingColDef?: Partial<Omit<GridBaseColDef<R>, 'field'>>;
282
+ /** Depth expanded on first render. `0` collapses every group; `-1` expands all. @default 0 */
283
+ defaultGroupingExpansionDepth?: number;
284
+ /** Takes priority over `defaultGroupingExpansionDepth` for the initial state of a group. */
285
+ isGroupExpandedByDefault?: (node: GridTreeNodeParams<R>) => boolean;
286
+ /**
287
+ * How selection spreads between parents and children with `treeData`. Unlike MUI, rows hidden
288
+ * by a filter stay selected, so a search-then-pick form keeps earlier picks.
289
+ */
290
+ rowSelectionPropagation?: GridRowSelectionPropagation;
291
+ /**
292
+ * Content of the expandable panel under a row (master-detail). Adds a toggle column at the
293
+ * start. Rows for which this returns `null` or `undefined` get no toggle.
294
+ */
295
+ getDetailPanelContent?: (params: GridRowParams<R>) => ReactNode;
296
+ /** Ids of rows whose detail panel is open (controlled). */
297
+ detailPanelExpandedRowIds?: ReadonlySet<GridRowId>;
298
+ defaultDetailPanelExpandedRowIds?: ReadonlySet<GridRowId>;
299
+ onDetailPanelExpandedRowIdsChange?: (ids: Set<GridRowId>) => void;
229
300
  /** Replaces the default centered "No rows" message. Shown when `rows` is empty and not loading. */
230
301
  noRowsOverlay?: ReactNode;
231
302
  /** Replaces the default skeleton rows shown when `loading` is true. */
@@ -239,6 +310,11 @@ export interface DataGridProps<R extends GridValidRowModel = GridValidRowModel>
239
310
 
240
311
  export const SELECT_COLUMN_ID = '__select__';
241
312
 
313
+ /** Same id as MUI X's master-detail toggle column. */
314
+ export const DETAIL_PANEL_TOGGLE_ID = '__detail_panel_toggle__';
315
+
316
+ export const EMPTY_ROW_ID_SET: ReadonlySet<GridRowId> = new Set();
317
+
242
318
  /** MUI X Data Grid checkbox-column field; accepted as an alias for {@link SELECT_COLUMN_ID}. */
243
319
  export const MUI_CHECKBOX_FIELD = '__check__';
244
320
 
@@ -1,12 +1,14 @@
1
- import { useMemo } from 'react';
1
+ import { useMemo, useState } from 'react';
2
2
  import {
3
3
  columnFilteringFeature,
4
4
  columnPinningFeature,
5
+ columnVisibilityFeature,
5
6
  createFilteredRowModel,
6
7
  createPaginatedRowModel,
7
8
  createSortedRowModel,
8
9
  filterFn_includesString,
9
10
  globalFilteringFeature,
11
+ rowExpandingFeature,
10
12
  rowPaginationFeature,
11
13
  rowSelectionFeature,
12
14
  rowSortingFeature,
@@ -20,7 +22,16 @@ import {
20
22
  type RowSelectionState,
21
23
  type SortingState,
22
24
  } from '@tanstack/react-table';
23
- import { createSelectColumn } from './adapter/selectColumn';
25
+ import { createDetailPanelToggleColumn, createSelectColumn } from './adapter/selectColumn';
26
+ import {
27
+ applyTreeSelection,
28
+ buildTreeIndex,
29
+ computeTreeSelection,
30
+ keepMatchesWithAncestors,
31
+ selectionScope,
32
+ type GridTreeRowLike,
33
+ type GridTreeSelectionStatus,
34
+ } from './adapter/treeData';
24
35
  import { applyUpdater, gridColDefToColumnDef } from './adapter/columns';
25
36
  import {
26
37
  filterFn_displayText,
@@ -42,6 +53,7 @@ import {
42
53
  } from './adapter/filterOperators';
43
54
  import type {
44
55
  GridColDef,
56
+ GridColumnVisibilityModel,
45
57
  GridFeatureMode,
46
58
  GridFilterItem,
47
59
  GridFilterModel,
@@ -49,18 +61,20 @@ import type {
49
61
  GridPaginationModel,
50
62
  GridRowId,
51
63
  GridRowSelectionModel,
64
+ GridRowSelectionPropagation,
52
65
  GridRowsProp,
53
66
  GridSortModel,
67
+ GridTreeNodeParams,
54
68
  GridValidRowModel,
55
69
  } from './types';
56
- import { SELECT_COLUMN_ID } from './types';
70
+ import { DETAIL_PANEL_TOGGLE_ID, SELECT_COLUMN_ID } from './types';
57
71
 
58
72
  function gridGlobalFilterFn(
59
73
  row: Parameters<typeof filterFn_displayText>[0],
60
74
  columnId: string,
61
75
  filterValue: unknown,
62
76
  ): boolean {
63
- if (columnId === SELECT_COLUMN_ID) {
77
+ if (columnId === SELECT_COLUMN_ID || columnId === DETAIL_PANEL_TOGGLE_ID) {
64
78
  return false;
65
79
  }
66
80
  const meta = row.getAllCells().find((cell) => cell.column.id === columnId)?.column.columnDef.meta;
@@ -73,7 +87,9 @@ function gridGlobalFilterFn(
73
87
  const features = tableFeatures({
74
88
  columnFilteringFeature,
75
89
  columnPinningFeature,
90
+ columnVisibilityFeature,
76
91
  globalFilteringFeature,
92
+ rowExpandingFeature,
77
93
  rowSortingFeature,
78
94
  rowPaginationFeature,
79
95
  rowSelectionFeature,
@@ -275,8 +291,30 @@ interface UseDataGridTableParams<R extends GridValidRowModel> {
275
291
  rowSelectionModel: GridRowSelectionModel;
276
292
  onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
277
293
  columnPinning: ColumnPinningState;
294
+ columnVisibility?: GridColumnVisibilityModel;
295
+ detailPanel?: boolean;
296
+ treeData?: boolean;
297
+ getTreeDataPath?: (row: R) => readonly string[];
298
+ defaultGroupingExpansionDepth?: number;
299
+ isGroupExpandedByDefault?: (node: GridTreeNodeParams<R>) => boolean;
300
+ rowSelectionPropagation?: GridRowSelectionPropagation;
301
+ }
302
+
303
+ export interface DataGridTreeState {
304
+ isRowExpanded: (key: string) => boolean;
305
+ toggleRowExpanded: (key: string) => void;
306
+ selectionStatus: (key: string) => GridTreeSelectionStatus;
307
+ toggleRowSelected: (row: GridTreeRowLike) => void;
308
+ toggleAllRowsSelected: () => void;
309
+ headerSelection: { checked: boolean; indeterminate: boolean };
278
310
  }
279
311
 
312
+ const EMPTY_EXPANSION: Readonly<Record<string, boolean>> = {};
313
+ const EMPTY_FILTER_EXPANSION: Readonly<Partial<Record<string, boolean>>> = {};
314
+ const EMPTY_VISIBILITY: GridColumnVisibilityModel = {};
315
+
316
+ let warnedMissingTreeDataPath = false;
317
+
280
318
  export function useDataGridTable<R extends GridValidRowModel>({
281
319
  rows,
282
320
  columns,
@@ -299,7 +337,22 @@ export function useDataGridTable<R extends GridValidRowModel>({
299
337
  rowSelectionModel,
300
338
  onRowSelectionModelChange,
301
339
  columnPinning,
340
+ columnVisibility = EMPTY_VISIBILITY,
341
+ detailPanel = false,
342
+ treeData: treeDataProp = false,
343
+ getTreeDataPath,
344
+ defaultGroupingExpansionDepth = 0,
345
+ isGroupExpandedByDefault,
346
+ rowSelectionPropagation,
302
347
  }: UseDataGridTableParams<R>) {
348
+ const treeData = treeDataProp && getTreeDataPath !== undefined;
349
+ if (__DEV__ && treeDataProp && getTreeDataPath === undefined && !warnedMissingTreeDataPath) {
350
+ warnedMissingTreeDataPath = true;
351
+ console.warn('[DataGrid] `treeData` needs `getTreeDataPath`. Rendering flat rows.');
352
+ }
353
+ const propagateDescendants = rowSelectionPropagation?.descendants ?? true;
354
+ const propagateParents = rowSelectionPropagation?.parents ?? true;
355
+
303
356
  const orFilterItems = useMemo(() => {
304
357
  if (filterLogicOperator !== 'or' || filterMode === 'server') {
305
358
  return undefined;
@@ -308,6 +361,13 @@ export function useDataGridTable<R extends GridValidRowModel>({
308
361
  return active.length > 1 ? active : undefined;
309
362
  }, [filterLogicOperator, filterMode, filterModel]);
310
363
 
364
+ const keyOf = useMemo(() => (row: R) => selectionIdKey(resolveRowId(row, getRowId)), [getRowId]);
365
+
366
+ const fullTree = useMemo(
367
+ () => (treeData ? buildTreeIndex(rows, getTreeDataPath, keyOf) : undefined),
368
+ [getTreeDataPath, keyOf, rows, treeData],
369
+ );
370
+
311
371
  const data = useMemo(() => {
312
372
  if (!orFilterItems) {
313
373
  return [...rows];
@@ -315,14 +375,24 @@ export function useDataGridTable<R extends GridValidRowModel>({
315
375
  const filterableColumns = new Map(
316
376
  columns.filter((col) => col.filterable !== false).map((col) => [col.field, col]),
317
377
  );
318
- return rows.filter((row) =>
378
+ const matches = (row: R) =>
319
379
  orFilterItems.some((item) => {
320
380
  const col = filterableColumns.get(item.field);
321
381
  return col !== undefined && rowMatchesFilterItem(col, row, item);
322
- }),
323
- );
324
- }, [columns, orFilterItems, rows]);
382
+ });
383
+ return fullTree
384
+ ? keepMatchesWithAncestors(rows, fullTree, keyOf, matches)
385
+ : rows.filter(matches);
386
+ }, [columns, fullTree, keyOf, orFilterItems, rows]);
325
387
  const allIds = useMemo(() => data.map((row) => resolveRowId(row, getRowId)), [data, getRowId]);
388
+ const allRowIds = useMemo(() => rows.map((row) => resolveRowId(row, getRowId)), [rows, getRowId]);
389
+
390
+ const tree = useMemo(() => {
391
+ if (!fullTree || !getTreeDataPath) {
392
+ return undefined;
393
+ }
394
+ return data.length === rows.length ? fullTree : buildTreeIndex(data, getTreeDataPath, keyOf);
395
+ }, [data, fullTree, getTreeDataPath, keyOf, rows.length]);
326
396
 
327
397
  const columnDefs = useMemo(() => {
328
398
  const mapped = columns.map((col) => {
@@ -332,8 +402,12 @@ export function useDataGridTable<R extends GridValidRowModel>({
332
402
  }
333
403
  return def;
334
404
  });
335
- return checkboxSelection ? [createSelectColumn(), ...mapped] : mapped;
336
- }, [checkboxSelection, columns, disableColumnSorting]);
405
+ return [
406
+ ...(checkboxSelection && !treeData ? [createSelectColumn()] : []),
407
+ ...(detailPanel ? [createDetailPanelToggleColumn()] : []),
408
+ ...mapped,
409
+ ];
410
+ }, [checkboxSelection, columns, detailPanel, disableColumnSorting, treeData]);
337
411
 
338
412
  const sortingState = useMemo(() => sortModelToSortingState(sortModel), [sortModel]);
339
413
  const paginationState = useMemo(() => paginationModelToState(paginationModel), [paginationModel]);
@@ -342,10 +416,79 @@ export function useDataGridTable<R extends GridValidRowModel>({
342
416
  [filterModel, orFilterItems],
343
417
  );
344
418
  const rowSelectionState = useMemo(
345
- () => selectionModelToRowSelection(rowSelectionModel, allIds),
346
- [rowSelectionModel, allIds],
419
+ () => selectionModelToRowSelection(rowSelectionModel, treeData ? allRowIds : allIds),
420
+ [rowSelectionModel, treeData, allRowIds, allIds],
347
421
  );
348
422
 
423
+ const filterSignature =
424
+ treeData &&
425
+ filterMode === 'client' &&
426
+ (quickFilter.trim().length > 0 || filterModel.some(isActiveFilterItem))
427
+ ? JSON.stringify([quickFilter, filterModel, filterLogicOperator])
428
+ : '';
429
+ const [userExpanded, setUserExpanded] = useState<Partial<Record<string, boolean>>>({});
430
+ const [filterExpansion, setFilterExpansion] = useState<{
431
+ signature: string;
432
+ expanded: Partial<Record<string, boolean>>;
433
+ }>({ signature: '', expanded: {} });
434
+ const filterExpanded =
435
+ filterSignature !== '' && filterExpansion.signature === filterSignature
436
+ ? filterExpansion.expanded
437
+ : EMPTY_FILTER_EXPANSION;
438
+
439
+ const expandedState = useMemo(() => {
440
+ if (!tree) {
441
+ return EMPTY_EXPANSION;
442
+ }
443
+ const isExpanded = (key: string): boolean => {
444
+ if (filterSignature !== '') {
445
+ return filterExpanded[key] ?? true;
446
+ }
447
+ const own = userExpanded[key];
448
+ if (own !== undefined) {
449
+ return own;
450
+ }
451
+ const row = tree.rowOf.get(key);
452
+ const depth = fullTree?.depthOf.get(key) ?? tree.depthOf.get(key) ?? 0;
453
+ if (row !== undefined && isGroupExpandedByDefault) {
454
+ return isGroupExpandedByDefault({ id: resolveRowId(row, getRowId), row, depth });
455
+ }
456
+ return defaultGroupingExpansionDepth === -1 || depth < defaultGroupingExpansionDepth;
457
+ };
458
+ const state: Record<string, boolean> = {};
459
+ for (const key of tree.childKeysOf.keys()) {
460
+ if (isExpanded(key)) {
461
+ state[key] = true;
462
+ }
463
+ }
464
+ return state;
465
+ }, [
466
+ tree,
467
+ fullTree,
468
+ filterSignature,
469
+ filterExpanded,
470
+ userExpanded,
471
+ isGroupExpandedByDefault,
472
+ defaultGroupingExpansionDepth,
473
+ getRowId,
474
+ ]);
475
+ const isRowExpanded = (key: string) => key in expandedState;
476
+
477
+ const toggleRowExpanded = (key: string) => {
478
+ const next = !isRowExpanded(key);
479
+ if (filterSignature !== '') {
480
+ setFilterExpansion((previous) => ({
481
+ signature: filterSignature,
482
+ expanded: {
483
+ ...(previous.signature === filterSignature ? previous.expanded : {}),
484
+ [key]: next,
485
+ },
486
+ }));
487
+ return;
488
+ }
489
+ setUserExpanded((previous) => ({ ...previous, [key]: next }));
490
+ };
491
+
349
492
  const resetPaginationPage = () => {
350
493
  if (paginationModel.page !== 0) {
351
494
  onPaginationModelChange({ ...paginationModel, page: 0 });
@@ -369,8 +512,11 @@ export function useDataGridTable<R extends GridValidRowModel>({
369
512
  const table = useTable({
370
513
  features,
371
514
  columns: columnDefs,
372
- data,
373
- getRowId: (row) => selectionIdKey(resolveRowId(row, getRowId)),
515
+ data: tree ? tree.roots : data,
516
+ getRowId: (row) => keyOf(row),
517
+ getSubRows: tree ? (row) => tree.childrenOf.get(row) : undefined,
518
+ filterFromLeafRows: treeData,
519
+ paginateExpandedRows: false,
374
520
  enableMultiSort: false,
375
521
  enableMultiRowSelection: true,
376
522
  globalFilterFn: gridGlobalFilterFn,
@@ -386,6 +532,8 @@ export function useDataGridTable<R extends GridValidRowModel>({
386
532
  globalFilter: quickFilter,
387
533
  rowSelection: rowSelectionState,
388
534
  columnPinning,
535
+ columnVisibility,
536
+ expanded: expandedState,
389
537
  },
390
538
  onSortingChange: (updater) => {
391
539
  const next = applyUpdater(updater, sortingState);
@@ -416,5 +564,59 @@ export function useDataGridTable<R extends GridValidRowModel>({
416
564
  resetPaginationPage();
417
565
  };
418
566
 
419
- return { table, setQuickFilter, setColumnFilter, setFilterModel };
567
+ const treeSelection = useMemo(() => {
568
+ if (!fullTree) {
569
+ return undefined;
570
+ }
571
+ const selectedKeys = new Set(Object.keys(rowSelectionState));
572
+ return {
573
+ selectedKeys,
574
+ statuses: computeTreeSelection(fullTree, selectedKeys, propagateParents),
575
+ };
576
+ }, [fullTree, propagateParents, rowSelectionState]);
577
+
578
+ let treeState: DataGridTreeState | undefined;
579
+ if (fullTree && treeSelection) {
580
+ const { selectedKeys, statuses } = treeSelection;
581
+ const selectionStatus = (key: string) => statuses.get(key) ?? 'none';
582
+ const commit = (scope: { targets: string[]; units: string[] }) => {
583
+ const value = !scope.units.every((key) => selectionStatus(key) === 'all');
584
+ const next = applyTreeSelection(
585
+ fullTree,
586
+ selectedKeys,
587
+ scope.targets,
588
+ value,
589
+ propagateParents,
590
+ );
591
+ onRowSelectionModelChange(
592
+ rowSelectionToModel(
593
+ Object.fromEntries([...next].map((key) => [key, true])),
594
+ rowSelectionModel,
595
+ allRowIds,
596
+ ),
597
+ );
598
+ };
599
+ const filtered = table.getFilteredRowModel();
600
+ const headerScope = propagateDescendants
601
+ ? selectionScope(filtered.rows, fullTree, true)
602
+ : selectionScope(filtered.flatRows, fullTree, false);
603
+ const selectedUnits = headerScope.units.filter((key) => selectionStatus(key) !== 'none');
604
+ const allUnitsSelected =
605
+ headerScope.units.length > 0 &&
606
+ headerScope.units.every((key) => selectionStatus(key) === 'all');
607
+
608
+ treeState = {
609
+ isRowExpanded,
610
+ toggleRowExpanded,
611
+ selectionStatus,
612
+ toggleRowSelected: (row) => commit(selectionScope([row], fullTree, propagateDescendants)),
613
+ toggleAllRowsSelected: () => commit(headerScope),
614
+ headerSelection: {
615
+ checked: allUnitsSelected,
616
+ indeterminate: !allUnitsSelected && selectedUnits.length > 0,
617
+ },
618
+ };
619
+ }
620
+
621
+ return { table, setQuickFilter, setColumnFilter, setFilterModel, tree: treeState };
420
622
  }