@opencorestack/opengridx 3.9.0 → 3.10.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +11 -7
  3. package/dist/index.d.ts +55 -0
  4. package/dist/opengridx.es.js +6346 -6279
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +4 -0
  9. package/docs/architecture/datagrid-orchestration.md +4 -2
  10. package/docs/architecture/use-grid-virtualization.md +2 -0
  11. package/docs/components/pagination.md +1 -1
  12. package/docs/features/ai-toolkit.md +1 -0
  13. package/docs/features/rtl.md +1 -1
  14. package/docs/performance.md +19 -1
  15. package/docs/roadmap.md +26 -5
  16. package/lib/components/Cell/Cell.tsx +2 -1
  17. package/lib/components/DataGrid/DataGrid.tsx +19 -18
  18. package/lib/components/DataGrid/GridListView.tsx +1 -1
  19. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  20. package/lib/components/DataGrid/GridViewport.tsx +2 -1
  21. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  22. package/lib/components/Header/Header.tsx +4 -3
  23. package/lib/components/Row/Row.tsx +6 -2
  24. package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
  25. package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
  26. package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
  27. package/lib/components/Toolbar/GridToolbar.tsx +7 -380
  28. package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
  29. package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
  30. package/lib/hooks/core/gridKeyDown.ts +272 -0
  31. package/lib/hooks/core/useDataGrid.ts +13 -5
  32. package/lib/hooks/core/useGridApiMethods.ts +2 -2
  33. package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
  34. package/lib/hooks/core/useGridCellSelection.ts +17 -297
  35. package/lib/hooks/core/useGridColumnLookup.ts +2 -1
  36. package/lib/hooks/core/useGridColumns.tsx +3 -2
  37. package/lib/hooks/core/useGridControlledState.ts +3 -2
  38. package/lib/hooks/core/useGridHierarchy.ts +1 -1
  39. package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
  40. package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
  41. package/lib/hooks/core/useGridRowInteractions.ts +1 -1
  42. package/lib/hooks/core/useGridRowSelection.ts +11 -2
  43. package/lib/hooks/core/useGridToolbarProps.ts +3 -2
  44. package/lib/hooks/features/useGridAiAssistant.ts +3 -2
  45. package/lib/hooks/useColumnReorder.ts +2 -1
  46. package/lib/hooks/useTreeData.ts +4 -2
  47. package/lib/types/locale.ts +22 -0
  48. package/lib/utils/aggregation/index.ts +16 -0
  49. package/lib/utils/aiAssistant.ts +46 -15
  50. package/lib/utils/export/excelCells.ts +244 -0
  51. package/lib/utils/export/exportCsv.ts +179 -0
  52. package/lib/utils/export/exportExcelHtml.ts +220 -0
  53. package/lib/utils/export/exportJson.ts +179 -0
  54. package/lib/utils/export/exportRows.ts +85 -0
  55. package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
  56. package/lib/utils/export/index.ts +5 -695
  57. package/lib/utils/export/printGrid.ts +282 -0
  58. package/lib/utils/gridLocale.ts +30 -0
  59. package/lib/utils/selectionSets.ts +36 -0
  60. package/lib/utils/untypedColumns.ts +16 -0
  61. package/llms.txt +3 -1
  62. package/package.json +6 -2
  63. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +0 -1
  64. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +0 -1
  65. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +0 -1
@@ -1,22 +1,33 @@
1
- import { useState, useCallback, useMemo, useRef, useLayoutEffect } from 'react';
2
- import { getScrollStart, isRtl, logicalArrowKey, setScrollStart } from '../../utils/direction';
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ useMemo,
5
+ useRef,
6
+ useLayoutEffect,
7
+ } from 'react';
8
+ import { handleGridKeyDown } from './gridKeyDown';
9
+ import { getScrollStart, setScrollStart } from '../../utils/direction';
3
10
  import { scrollRowIntoView } from '../../utils/scroll';
4
- import { resolveCellEditable } from '../../utils/editing';
5
- import { getCellValue } from '../../utils/values';
6
- import { isSyntheticRowId } from '../../utils/syntheticRows';
7
11
  import {
8
12
  CHECKBOX_FIELD,
9
13
  EXPAND_FIELD,
10
14
  REORDER_FIELD,
11
- classifyKeyTarget,
12
15
  findFocusTarget,
13
16
  focusedCellFromElement,
14
17
  isSameCell,
15
18
  isSystemField,
16
19
  } from '../../utils/focus';
17
20
  import type { FocusedCell } from '../../utils/focus';
18
- import { isRangeColumnField } from '../../utils/cellSelection';
19
- import type { GridRowModel, GridRowId, GridColDef, GridCellParams, GridSortItem, GridSortDirection, GridRowParams, GridRowMeta } from '../../types';
21
+ import type {
22
+ GridRowModel,
23
+ GridRowId,
24
+ GridColDef,
25
+ GridCellParams,
26
+ GridSortItem,
27
+ GridSortDirection,
28
+ GridRowParams,
29
+ GridRowMeta,
30
+ } from '../../types';
20
31
  import type { GridEditingState } from '../features/useGridEditing';
21
32
 
22
33
  export type { FocusedCell } from '../../utils/focus';
@@ -44,7 +55,7 @@ export interface UseGridKeyboardNavigationParams<R extends GridRowModel> {
44
55
  /** Focusable columns in render order: system columns first, then the visible data columns. */
45
56
  navigationColumns: ReadonlyArray<NavigationColumn<R>>;
46
57
  checkboxSelection: boolean;
47
- selectedRowIds: Set<GridRowId>;
58
+ selectedRowIds: ReadonlySet<GridRowId>;
48
59
  handleSelectionChange: (id: GridRowId, selected: boolean) => void;
49
60
  handleDetailPanelToggle: (id: GridRowId) => void;
50
61
  editingHandlers: {
@@ -133,8 +144,6 @@ export interface UseGridKeyboardNavigationReturn {
133
144
 
134
145
  const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
135
146
 
136
- const NAVIGATION_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown']);
137
-
138
147
  /** Keeps a stored focus position valid when its row or column is no longer rendered. */
139
148
  function resolveFocus<R extends GridRowModel>(
140
149
  cell: FocusedCell | null,
@@ -359,230 +368,37 @@ export function useGridKeyboardNavigation<R extends GridRowModel>(
359
368
  }, [setKeyboardMode]);
360
369
 
361
370
  const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
362
- const viewport = event.currentTarget as HTMLElement;
363
- const targetKind = classifyKeyTarget(event.target, viewport);
364
- if (targetKind === 'foreign') return;
365
- setKeyboardMode(true);
366
-
367
- // While an IME is composing, Enter / Escape / arrows confirm or navigate candidates.
368
- if (event.nativeEvent?.isComposing || event.keyCode === 229) return;
369
-
370
- // In a right-to-left grid ArrowLeft moves to the column on the left: the next column.
371
- const key = logicalArrowKey(event.key, isRtl(viewport));
372
- const isSpace = key === ' ' || key === 'Spacebar';
373
- const isEditing = Boolean(editingHandlers.editingCell);
374
-
375
- // Row positions: rows from 0, the column header row at `headerRow`, and the header filter
376
- // row (when rendered) at -1, between the header and the rows.
377
- const headerRow = headerFilters ? -2 : -1;
378
- const moveTo = (r: number, c: number) => {
379
- const col = navigationColumns[c];
380
- if (!col || r < headerRow || r >= allRenderableRows.length) return;
381
- if (r === headerRow) setFocusedCell({ id: null, field: col.field });
382
- else if (r < 0) setFocusedCell({ id: null, field: col.field, headerFilter: true });
383
- else setFocusedCell({ id: getRowId(allRenderableRows[r]), field: col.field, rowIndex: r });
384
- scrollCellIntoView(r, col.field);
385
- };
386
-
387
- if (!focusedCell) {
388
- if (!NAVIGATION_KEYS.has(key)) return;
389
- const initial = getDefaultFocus();
390
- if (!initial) return;
391
- event.preventDefault();
392
- setFocusedCell(initial);
393
- scrollCellIntoView(initial.rowIndex ?? -1, initial.field);
394
- return;
395
- }
396
-
397
- const { id, field } = focusedCell;
398
- const onHeaderFilter = id === null && Boolean(focusedCell.headerFilter);
399
- const rowIndex = id !== null ? focusedRowIndex : onHeaderFilter ? -1 : headerRow;
400
- const colIndex = navigationColumns.findIndex(c => c.field === field);
401
- const systemColumnCount = navigationColumns.filter(c => isSystemField(c.field)).length;
402
- const isSyntheticRow = (rowId: GridRowId) => isSyntheticRowId(rowId, rowMetaMap);
403
- // Same rule and starting value as double-click (see Row / resolveCellEditable).
404
- const isEditableCell = (row: R, r: number, c: number) => {
405
- const colDef = navigationColumns[c] as GridColDef<R>;
406
- if (isSystemField(colDef.field)) return false;
407
- return resolveCellEditable({
408
- row,
409
- field: colDef.field,
410
- value: getCellValue(row, colDef.field, colDef),
411
- colDef,
412
- rowIndex: r,
413
- colIndex: c - systemColumnCount,
414
- rowMeta: rowMetaMap?.get(getRowId(row)),
415
- }, isCellEditable);
416
- };
417
-
418
- // Next editable data cell in reading order, for Tab while editing.
419
- const findNextEditable = (deltaCol: number) => {
420
- let r = rowIndex;
421
- let c = colIndex;
422
- const maxSteps = (allRenderableRows.length + 1) * navigationColumns.length;
423
- for (let steps = 0; steps < maxSteps; steps++) {
424
- c += deltaCol;
425
- if (c >= navigationColumns.length) { c = 0; r++; }
426
- else if (c < 0) { c = navigationColumns.length - 1; r--; }
427
- if (r < 0 || r >= allRenderableRows.length) return null;
428
-
429
- const row = allRenderableRows[r];
430
- // Cells covered by a span are not stops; their origin is.
431
- if (getSpanOrigin?.(getRowId(row), navigationColumns[c].field)) continue;
432
- if (isEditableCell(row, r, c)) return { r, c };
433
- }
434
- return null;
435
- };
436
-
437
- if (isEditing) {
438
- if (key === 'Enter') {
439
- event.preventDefault();
440
- editingHandlers.stopCellEdit();
441
- } else if (key === 'Escape') {
442
- event.preventDefault();
443
- editingHandlers.stopCellEdit({ cancel: true });
444
- } else if (key === 'Tab' && id !== null) {
445
- // No next editable cell: the browser moves focus on and the editor commits on blur.
446
- const next = findNextEditable(event.shiftKey ? -1 : 1);
447
- if (next) {
448
- event.preventDefault();
449
- editingHandlers.stopCellEdit();
450
- moveTo(next.r, next.c);
451
- }
452
- }
453
- return;
454
- }
455
-
456
- // Tab is not captured outside edit mode: the grid is a single tab stop and Tab leaves it
457
- // (or reaches focusable content inside cells and detail panels).
458
- if (key === 'Escape' && cellSelection?.collapseSelection()) {
459
- event.preventDefault();
460
- return;
461
- }
462
- if (key === 'Tab' || key === 'Escape') return;
463
- // Enter and Space activate a control rendered inside a cell.
464
- if (targetKind === 'control' && (key === 'Enter' || isSpace)) return;
465
-
466
- if ((event.ctrlKey || event.metaKey) && (key === 'a' || key === 'A')) {
467
- if (cellSelection) {
468
- if (cellSelection.selectAllCells()) event.preventDefault();
469
- return;
470
- }
471
- if (!rowSelectionEnabled || !multipleRowSelectionEnabled || !handleSelectAll) return;
472
- event.preventDefault();
473
- handleSelectAll(true);
474
- return;
475
- }
476
-
477
- // A filter cell handles Enter, Space and typing itself (HeaderFilterCell).
478
- if (onHeaderFilter && (key === 'Enter' || isSpace)) return;
479
- if (id === null) {
480
- if (key === 'Enter' || isSpace) {
481
- event.preventDefault();
482
- if (field === CHECKBOX_FIELD) {
483
- // Same as clicking the select-all checkbox.
484
- findFocusTarget(viewport, focusedCell, -1)?.click();
485
- return;
486
- }
487
- const col = navigationColumns[colIndex];
488
- if (!col || isSystemField(field) || col.sortable === false) return;
489
- const currentSort = sortModel.find(item => item.field === field);
490
- const direction: GridSortDirection = currentSort ? (currentSort.sort === 'asc' ? 'desc' : null) : 'asc';
491
- if ((multiSort || event.shiftKey) && handleSortAdd) handleSortAdd(field, direction);
492
- else handleSort(field, direction);
493
- return;
494
- }
495
- } else if (key === 'Enter' || isSpace) {
496
- event.preventDefault();
497
- const row = allRenderableRows[rowIndex];
498
- if (field === CHECKBOX_FIELD) {
499
- handleSelectionChange(id, !selectedRowIds.has(id));
500
- return;
501
- }
502
- if (field === EXPAND_FIELD) {
503
- if (!event.shiftKey) handleDetailPanelToggle(id);
504
- return;
505
- }
506
- if (isSpace) {
507
- // Shift+Space selects the row; plain Space only must not scroll the viewport.
508
- if (event.shiftKey && rowSelectionEnabled && !isSyntheticRow(id)) {
509
- handleSelectionChange(id, !selectedRowIds.has(id));
510
- }
511
- return;
512
- }
513
- if (field === REORDER_FIELD) return;
514
- if (isEditableCell(row, rowIndex, colIndex)) {
515
- editingHandlers.startCellEdit({ id, field, value: getCellValue(row, field, navigationColumns[colIndex] as GridColDef<R>) });
516
- return;
517
- }
518
- // A row with children toggles, so keyboard users can expand a tree-data parent even
519
- // though clicking it selects it. Others: the keyboard equivalent of clicking the row.
520
- if (toggleRowExpansion && rowMetaMap?.get(id)?.hasChildren) {
521
- toggleRowExpansion(id);
522
- return;
523
- }
524
- onRowActivate?.({ row, id, rowIndex });
525
- return;
526
- }
527
-
528
- // Treegrid shortcuts: Alt+ArrowRight expands, Alt+ArrowLeft collapses the focused row.
529
- if (event.altKey && (key === 'ArrowRight' || key === 'ArrowLeft') && toggleRowExpansion && id !== null) {
530
- event.preventDefault();
531
- const meta = rowMetaMap?.get(id);
532
- if (meta?.hasChildren && Boolean(meta.isExpanded) !== (key === 'ArrowRight')) toggleRowExpansion(id);
533
- return;
534
- }
535
-
536
- if (!NAVIGATION_KEYS.has(key)) return;
537
- // Prevent native scrolling even when the focus cannot move (grid edges).
538
- event.preventDefault();
539
-
540
- const lastRow = allRenderableRows.length - 1;
541
- const lastCol = navigationColumns.length - 1;
542
- const withModifier = event.ctrlKey || event.metaKey;
543
- const page = pagination ? pageSize : 10;
544
-
545
- // Shift+navigation on a data cell extends the cell range; focus stays on its anchor.
546
- if (cellSelection && event.shiftKey && id !== null && isRangeColumnField(field)
547
- && cellSelection.extendSelection(key, withModifier, page, focusedCell)) {
548
- return;
549
- }
550
-
551
- const step = (r: number, c: number): { r: number; c: number } => {
552
- switch (key) {
553
- case 'ArrowRight': return c + 1 > lastCol ? { r: r + 1, c: 0 } : { r, c: c + 1 };
554
- case 'ArrowLeft': return c - 1 < 0 ? { r: r - 1, c: lastCol } : { r, c: c - 1 };
555
- case 'ArrowDown': return { r: r + 1, c };
556
- case 'ArrowUp': return { r: r - 1, c };
557
- case 'Home': return { r: withModifier ? headerRow : r, c: 0 };
558
- case 'End': return { r: withModifier ? lastRow : r, c: lastCol };
559
- case 'PageUp': return { r: Math.max(headerRow, r - page), c };
560
- default: return { r: Math.min(lastRow, r + page), c };
561
- }
562
- };
563
- const inGrid = ({ r, c }: { r: number; c: number }) => r >= headerRow && r <= lastRow && c >= 0 && c <= lastCol;
564
- const spanOriginAt = ({ r, c }: { r: number; c: number }) =>
565
- r >= 0 ? getSpanOrigin?.(getRowId(allRenderableRows[r]), navigationColumns[c].field) ?? null : null;
566
-
567
- let next = step(rowIndex, colIndex);
568
- // Arrow keys step over the cells of the focused span (its own merged area) ...
569
- if (key.startsWith('Arrow')) {
570
- for (let origin = inGrid(next) ? spanOriginAt(next) : null;
571
- origin && origin.rowId === id && origin.field === field;
572
- origin = inGrid(next) ? spanOriginAt(next) : null) {
573
- next = step(next.r, next.c);
574
- }
575
- }
576
- if (!inGrid(next)) return;
577
- // ... then land on the origin of whatever span covers the target cell.
578
- const origin = spanOriginAt(next);
579
- if (origin) {
580
- const originRow = allRenderableRows.findIndex(r => getRowId(r) === origin.rowId);
581
- const originCol = navigationColumns.findIndex(c => c.field === origin.field);
582
- if (originRow !== -1 && originCol !== -1) next = { r: originRow, c: originCol };
583
- }
584
- if (next.r === rowIndex && next.c === colIndex) return;
585
- moveTo(next.r, next.c);
371
+ handleGridKeyDown(event, {
372
+ focusedCell,
373
+ focusedRowIndex,
374
+ allRenderableRows,
375
+ navigationColumns,
376
+ editingHandlers,
377
+ selectedRowIds,
378
+ handleSelectionChange,
379
+ handleDetailPanelToggle,
380
+ setKeyboardMode,
381
+ sortModel,
382
+ handleSort,
383
+ handleSortAdd,
384
+ multiSort,
385
+ isCellEditable,
386
+ pagination,
387
+ pageSize,
388
+ rowMetaMap,
389
+ onRowActivate,
390
+ toggleRowExpansion,
391
+ handleSelectAll,
392
+ rowSelectionEnabled,
393
+ multipleRowSelectionEnabled,
394
+ getDefaultFocus,
395
+ scrollCellIntoView,
396
+ getSpanOrigin,
397
+ getRowId,
398
+ cellSelection,
399
+ headerFilters,
400
+ setFocusedCell,
401
+ });
586
402
  }, [
587
403
  focusedCell,
588
404
  focusedRowIndex,
@@ -3,7 +3,7 @@ import type { GridRowId, GridRowMeta, GridRowModel } from '../../types';
3
3
 
4
4
  export interface UseGridLiveRowSelectionParams {
5
5
  /** The selection model (controlled prop or internal state). */
6
- selectedRowIds: Set<GridRowId>;
6
+ selectedRowIds: ReadonlySet<GridRowId>;
7
7
  /** The row store: ids of the rows the grid holds. */
8
8
  rowsLookup: ReadonlyMap<GridRowId, GridRowModel>;
9
9
  /** Hierarchy rows the grid made (group rows, generated tree parents); their ids are kept. */
@@ -32,7 +32,7 @@ const idsKey = (ids: Iterable<GridRowId>): string =>
32
32
  * own state that re-adds them) is not notified again and cannot loop, and StrictMode's re-run of
33
33
  * the effect does not report twice.
34
34
  */
35
- export function useGridLiveRowSelection(params: UseGridLiveRowSelectionParams): Set<GridRowId> {
35
+ export function useGridLiveRowSelection(params: UseGridLiveRowSelectionParams): ReadonlySet<GridRowId> {
36
36
  const { selectedRowIds, rowsLookup, rowMetaMap, enabled, onSelectionModelChange } = params;
37
37
 
38
38
  const liveRowIds = useMemo(() => {
@@ -10,7 +10,7 @@ export interface UseGridRowInteractionsParams<R extends GridRowModel> {
10
10
  dataRows: R[];
11
11
  getRowId: (row: GridRowModel) => GridRowId;
12
12
  isPivotActive: boolean;
13
- selectedRowIds: Set<GridRowId>;
13
+ selectedRowIds: ReadonlySet<GridRowId>;
14
14
  onSelectionModelChange: (model: GridRowId[]) => void;
15
15
  disableMultipleRowSelection: boolean;
16
16
  disableRowSelectionOnClick: boolean;
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
  import type { GridRowId } from '../../types';
3
+ import { isKnownToContainAll, markContainsAll, rememberSelectionSet } from '../../utils/selectionSets';
3
4
 
4
5
  // ── Pure selection transitions (shared by the UI handlers and the imperative API) ──────────
5
6
 
@@ -15,7 +16,12 @@ export function nextSelectionForRows(
15
16
  }
16
17
  const next = new Set(current);
17
18
  ids.forEach(id => (isSelected ? next.add(id) : next.delete(id)));
18
- return Array.from(next);
19
+ const model = Array.from(next);
20
+ // The model's set is `next`: reused when this array comes back as rowSelectionModel, and known to
21
+ // contain `ids` when they were selected (select-all then skips counting them again).
22
+ rememberSelectionSet(model, next);
23
+ if (isSelected) markContainsAll(next, ids);
24
+ return model;
19
25
  }
20
26
 
21
27
  /** A row click toggles the row; with `single` it replaces the selection instead of adding to it. */
@@ -30,7 +36,7 @@ export function isSameSelection(current: ReadonlySet<GridRowId>, next: readonly
30
36
  }
31
37
 
32
38
  export interface UseGridRowSelectionParams {
33
- selectedRowIds: Set<GridRowId>;
39
+ selectedRowIds: ReadonlySet<GridRowId>;
34
40
  /** Applies a new model: internal state when uncontrolled, plus onRowSelectionModelChange. */
35
41
  onSelectionModelChange: (model: GridRowId[]) => void;
36
42
  disableMultipleRowSelection: boolean;
@@ -75,6 +81,9 @@ export function useGridRowSelection(params: UseGridRowSelectionParams): UseGridR
75
81
  }, [commit, selectedRowIds, selectableRowIds]);
76
82
 
77
83
  const { allSelected, someSelected } = useMemo(() => {
84
+ if (selectableRowIds.length > 0 && isKnownToContainAll(selectedRowIds, selectableRowIds)) {
85
+ return { allSelected: true, someSelected: false };
86
+ }
78
87
  let selectedCount = 0;
79
88
  for (const id of selectableRowIds) if (selectedRowIds.has(id)) selectedCount++;
80
89
  const all = selectableRowIds.length > 0 && selectedCount === selectableRowIds.length;
@@ -13,6 +13,7 @@ import type {
13
13
  GridRowModel,
14
14
  GridToolbarSlotProps,
15
15
  } from '../../types';
16
+ import { untypedColumns } from '../../utils/untypedColumns';
16
17
 
17
18
  type ToolbarSlot = NonNullable<DataGridProps['slots']>['toolbar'];
18
19
  type ToolbarSlotProps = NonNullable<DataGridProps['slotProps']>['toolbar'];
@@ -79,8 +80,8 @@ export function useGridToolbarProps<R extends GridRowModel>(
79
80
  };
80
81
  return {
81
82
  apiRef,
82
- columns: orderedColumns as unknown as GridColDef[],
83
- baseColumns: baseColumns as unknown as GridColDef[],
83
+ columns: untypedColumns(orderedColumns),
84
+ baseColumns: baseColumns && untypedColumns(baseColumns),
84
85
  aggregationModel,
85
86
  onAggregationModelChange,
86
87
  ...(pivotMode || pivotModelProp || onPivotModelChangeProp ? {
@@ -12,7 +12,7 @@ import type {
12
12
  GridColDef,
13
13
  GridLocaleText,
14
14
  } from '../../types';
15
- import { resolveGridLocale, type ResolvedGridLocale } from '../../utils/gridLocale';
15
+ import { aiChipTextsOf, resolveGridLocale, type ResolvedGridLocale } from '../../utils/gridLocale';
16
16
  import { attempt } from '../../utils/attempt';
17
17
  import { isGridAiValidated } from '../../utils/aiBrand';
18
18
  import {
@@ -143,7 +143,8 @@ export function useGridAiAssistant(params: UseGridAiAssistantParams): UseGridAiA
143
143
  const byField = new Map(columns.map((col) => [col.field, col.headerName || col.field]));
144
144
  return (field: string) => byField.get(field) ?? field;
145
145
  }, [columns]);
146
- const chips = useMemo(() => (applied ? buildAiChips(applied.state, labelOf) : []), [applied, labelOf]);
146
+ const chipTexts = useMemo(() => aiChipTextsOf(locale), [locale]);
147
+ const chips = useMemo(() => (applied ? buildAiChips(applied.state, labelOf, chipTexts) : []), [applied, labelOf, chipTexts]);
147
148
 
148
149
  const abortRequest = useCallback(() => {
149
150
  const controller = controllerRef.current;
@@ -1,6 +1,7 @@
1
1
  import { useState, useCallback, useRef, useEffect } from 'react';
2
2
  import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../types';
3
3
  import { COLUMN_DRAG_MIME, hasDragToken, onNativeDragEnd, setDragToken } from '../utils/dragData';
4
+ import { untypedColumn } from '../utils/untypedColumns';
4
5
 
5
6
  export interface UseColumnReorderProps<R extends GridRowModel = GridRowModel> {
6
7
  /** Every current column, in the grid's column order. Reported indices are positions in it. */
@@ -89,7 +90,7 @@ export function useColumnReorder<R extends GridRowModel = GridRowModel>(
89
90
  if (oldIndex === -1 || targetIndex === -1 || oldIndex === targetIndex) return;
90
91
 
91
92
  onColumnOrderChange?.({
92
- column: columns[oldIndex] as unknown as GridColDef,
93
+ column: untypedColumn(columns[oldIndex]),
93
94
  oldIndex,
94
95
  targetIndex,
95
96
  });
@@ -319,7 +319,9 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
319
319
  * indent and the labels of auto-created parents): sorting by it orders auto-created parents by
320
320
  * their label, since they have no cell values of their own.
321
321
  */
322
- const getVisibleRows = useCallback((options?: GridHierarchyVisibleRowsOptions): GridRowModel[] | null => {
322
+ // Generated parent rows (missing path ancestors) are rendered like the consumer's rows and typed as
323
+ // `R`, as row grouping's group rows are: the grid's documented convention for rendered rows.
324
+ const getVisibleRows = useCallback((options?: GridHierarchyVisibleRowsOptions): R[] | null => {
323
325
  if (!isActive) return null;
324
326
  const labelField = options?.labelField;
325
327
  const expandAll = options?.expandAll ?? false;
@@ -364,7 +366,7 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
364
366
  };
365
367
 
366
368
  traverse(rootIds);
367
- return result;
369
+ return result as R[];
368
370
  }, [isActive, visibility, isClientSort, sortModel, syntheticIds, treeNodes, rowLookup, columnLookup, rootIds, expandedGroupIds]);
369
371
 
370
372
  const isGroupExpanded = useCallback((id: GridRowId) => expandedGroupIds.has(id), [expandedGroupIds]);
@@ -364,6 +364,28 @@ export interface GridLocaleText {
364
364
  aiAssistantFailed?: (message: string) => string;
365
365
  /** Error text when a failed request has no message. Default: `"Unknown error"` @since v3.9 */
366
366
  aiAssistantUnknownError?: string;
367
+ /** Change chip for the quick-filter words. Default: `` (w) => `Search: ${w}` `` @since v3.10 */
368
+ aiAssistantChipSearch?: (words: string) => string;
369
+ /** Change chip when the reply cleared the filters. Default: `"No filters"` @since v3.10 */
370
+ aiAssistantChipNoFilters?: string;
371
+ /** Change chip for one sort key. Default: `` (f, d) => `Sort: ${f}, ascending|descending` `` @since v3.10 */
372
+ aiAssistantChipSort?: (field: string, direction: 'asc' | 'desc') => string;
373
+ /** Change chip when the reply cleared the sort. Default: `"No sorting"` @since v3.10 */
374
+ aiAssistantChipNoSorting?: string;
375
+ /** Change chip for one grouping field. Default: `` (f) => `Group: ${f}` `` @since v3.10 */
376
+ aiAssistantChipGroup?: (field: string) => string;
377
+ /** Change chip when the reply removed grouping. Default: `"No grouping"` @since v3.10 */
378
+ aiAssistantChipNoGrouping?: string;
379
+ /** Change chip for one summary; `fn` is the function id (`sum`, `avg`, …). Default: `` (fn, f) => `Sum: ${f}` `` (Sum, Average, Count, Min, Max, Unique) @since v3.10 */
380
+ aiAssistantChipSummary?: (fn: string, field: string) => string;
381
+ /** Change chip when the reply removed the summaries. Default: `"No summaries"` @since v3.10 */
382
+ aiAssistantChipNoSummaries?: string;
383
+ /** Change chip for the pivot; receives the column labels joined with ", ". Default: `` (f) => `Pivot: ${f}` `` @since v3.10 */
384
+ aiAssistantChipPivot?: (fields: string) => string;
385
+ /** Change chip when the reply turned the pivot off. Default: `"No pivot"` @since v3.10 */
386
+ aiAssistantChipNoPivot?: string;
387
+ /** Change chip for a column shown or hidden. Default: `` (f, v) => `Show: ${f}` `` / `` `Hide: ${f}` `` @since v3.10 */
388
+ aiAssistantChipVisibility?: (field: string, visible: boolean) => string;
367
389
 
368
390
  // ── Pagination (v3.9) ─────────────────────────────────────────────────────
369
391
  /** `aria-label` of the pagination bar. Default: `"Pagination"` @since v3.9 */
@@ -210,3 +210,19 @@ export function formatAggregateForColumn<R extends GridRowModel>(
210
210
  if (normalized instanceof Date) return Number.isNaN(normalized.getTime()) ? '' : formatLocalDate(normalized);
211
211
  return formatAggregationValue(normalized, fnName);
212
212
  }
213
+
214
+ const BUILT_IN_AGGREGATION_FUNCTIONS = Object.keys(AGGREGATION_FUNCTIONS) as BuiltInAggFn[];
215
+
216
+ function isBuiltInAggregationFunction(fn: string): fn is BuiltInAggFn {
217
+ return Object.prototype.hasOwnProperty.call(AGGREGATION_FUNCTIONS, fn);
218
+ }
219
+
220
+ /**
221
+ * The summary functions offered for a column: its `availableAggregationFunctions` when set,
222
+ * otherwise every built-in one. Names the aggregation engine does not implement are dropped,
223
+ * because choosing one would show the label in the header but never compute a value.
224
+ */
225
+ export function getOfferedAggregationFunctions(col: Pick<GridColDef, 'availableAggregationFunctions'>): BuiltInAggFn[] {
226
+ if (!col.availableAggregationFunctions) return BUILT_IN_AGGREGATION_FUNCTIONS;
227
+ return Array.from(new Set(col.availableAggregationFunctions.filter(isBuiltInAggregationFunction)));
228
+ }
@@ -119,47 +119,78 @@ function valueText(value: unknown): string {
119
119
  return value === undefined || value === null ? '' : String(value);
120
120
  }
121
121
 
122
- function filterLabel(node: GridFilterItem | GridFilterGroup, label: (field: string) => string): string {
123
- if ('items' in node) return `(${node.items.map((child) => filterLabel(child, label)).join(` ${node.logicOperator} `)})`;
124
- return [label(node.field), node.operator, valueText(node.value)].filter(Boolean).join(' ');
122
+ function filterLabel(node: GridFilterItem | GridFilterGroup, label: (field: string) => string, operator: (op: string) => string): string {
123
+ if ('items' in node) return `(${node.items.map((child) => filterLabel(child, label, operator)).join(` ${node.logicOperator} `)})`;
124
+ return [label(node.field), operator(node.operator), valueText(node.value)].filter(Boolean).join(' ');
125
125
  }
126
126
 
127
127
  const FN_LABELS: Record<string, string> = { sum: 'Sum', avg: 'Average', count: 'Count', min: 'Min', max: 'Max', unique: 'Unique' };
128
128
 
129
+ /** The chips' texts (the grid passes its `localeText`; English by default). */
130
+ export interface GridAiChipTexts {
131
+ operator: (operator: string) => string;
132
+ search: (words: string) => string;
133
+ noFilters: string;
134
+ sort: (field: string, direction: 'asc' | 'desc') => string;
135
+ noSorting: string;
136
+ group: (field: string) => string;
137
+ noGrouping: string;
138
+ summary: (fn: string, field: string) => string;
139
+ noSummaries: string;
140
+ pivot: (fields: string) => string;
141
+ noPivot: string;
142
+ visibility: (field: string, visible: boolean) => string;
143
+ }
144
+
145
+ export const AI_CHIP_TEXTS_EN: GridAiChipTexts = {
146
+ operator: op => op,
147
+ search: words => `Search: ${words}`,
148
+ noFilters: 'No filters',
149
+ sort: (field, direction) => `Sort: ${field}, ${direction === 'desc' ? 'descending' : 'ascending'}`,
150
+ noSorting: 'No sorting',
151
+ group: field => `Group: ${field}`,
152
+ noGrouping: 'No grouping',
153
+ summary: (fn, field) => `${FN_LABELS[fn] ?? fn}: ${field}`,
154
+ noSummaries: 'No summaries',
155
+ pivot: fields => `Pivot: ${fields}`,
156
+ noPivot: 'No pivot',
157
+ visibility: (field, visible) => `${visible ? 'Show' : 'Hide'}: ${field}`,
158
+ };
159
+
129
160
  /** One chip per change: filter item, quick-filter words, sort key, grouping field, aggregation, visibility change, pivot. */
130
- export function buildAiChips(applied: GridAiState, label: (field: string) => string): GridAiChip[] {
161
+ export function buildAiChips(applied: GridAiState, label: (field: string) => string, texts: GridAiChipTexts = AI_CHIP_TEXTS_EN): GridAiChip[] {
131
162
  const chips: GridAiChip[] = [];
132
163
  const { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel } = applied;
133
164
  if (filterModel) {
134
165
  const items = filterModel.items ?? [];
135
- items.forEach((item, i) => chips.push({ id: `filter:${i}`, part: 'filter', label: filterLabel(item, label) }));
166
+ items.forEach((item, i) => chips.push({ id: `filter:${i}`, part: 'filter', label: filterLabel(item, label, texts.operator) }));
136
167
  const words = filterModel.quickFilterValues ?? [];
137
- if (words.length > 0) chips.push({ id: 'filter:quick', part: 'filter', label: `Search: ${words.join(' ')}` });
138
- if (items.length === 0 && words.length === 0) chips.push({ id: 'filter:clear', part: 'filter', label: 'No filters' });
168
+ if (words.length > 0) chips.push({ id: 'filter:quick', part: 'filter', label: texts.search(words.join(' ')) });
169
+ if (items.length === 0 && words.length === 0) chips.push({ id: 'filter:clear', part: 'filter', label: texts.noFilters });
139
170
  }
140
171
  if (sortModel) {
141
- sortModel.forEach((s, i) => chips.push({ id: `sort:${i}`, part: 'sort', label: `Sort: ${label(s.field)}, ${s.sort === 'desc' ? 'descending' : 'ascending'}` }));
142
- if (sortModel.length === 0) chips.push({ id: 'sort:clear', part: 'sort', label: 'No sorting' });
172
+ sortModel.forEach((s, i) => chips.push({ id: `sort:${i}`, part: 'sort', label: texts.sort(label(s.field), s.sort === 'desc' ? 'desc' : 'asc') }));
173
+ if (sortModel.length === 0) chips.push({ id: 'sort:clear', part: 'sort', label: texts.noSorting });
143
174
  }
144
175
  if (rowGroupingModel) {
145
- rowGroupingModel.forEach((field, i) => chips.push({ id: `grouping:${i}`, part: 'grouping', label: `Group: ${label(field)}` }));
146
- if (rowGroupingModel.length === 0) chips.push({ id: 'grouping:clear', part: 'grouping', label: 'No grouping' });
176
+ rowGroupingModel.forEach((field, i) => chips.push({ id: `grouping:${i}`, part: 'grouping', label: texts.group(label(field)) }));
177
+ if (rowGroupingModel.length === 0) chips.push({ id: 'grouping:clear', part: 'grouping', label: texts.noGrouping });
147
178
  }
148
179
  if (aggregationModel) {
149
180
  const fields = Object.keys(aggregationModel);
150
181
  fields.forEach((field) => {
151
182
  const fn = aggregationModel[field];
152
- chips.push({ id: `aggregation:${field}`, part: 'aggregation', label: `${FN_LABELS[fn] ?? fn}: ${label(field)}` });
183
+ chips.push({ id: `aggregation:${field}`, part: 'aggregation', label: texts.summary(fn, label(field)) });
153
184
  });
154
- if (fields.length === 0) chips.push({ id: 'aggregation:clear', part: 'aggregation', label: 'No summaries' });
185
+ if (fields.length === 0) chips.push({ id: 'aggregation:clear', part: 'aggregation', label: texts.noSummaries });
155
186
  }
156
187
  if (pivotModel) {
157
188
  const fields = [...pivotModel.rowFields, ...pivotModel.columnFields, ...pivotModel.valueFields.map((v) => v.field)];
158
- chips.push({ id: 'pivot', part: 'pivot', label: fields.length > 0 ? `Pivot: ${fields.map(label).join(', ')}` : 'No pivot' });
189
+ chips.push({ id: 'pivot', part: 'pivot', label: fields.length > 0 ? texts.pivot(fields.map(label).join(', ')) : texts.noPivot });
159
190
  }
160
191
  if (columnVisibilityModel) {
161
192
  for (const field of Object.keys(columnVisibilityModel)) {
162
- chips.push({ id: `columnVisibility:${field}`, part: 'columnVisibility', label: `${columnVisibilityModel[field] ? 'Show' : 'Hide'}: ${label(field)}` });
193
+ chips.push({ id: `columnVisibility:${field}`, part: 'columnVisibility', label: texts.visibility(label(field), Boolean(columnVisibilityModel[field])) });
163
194
  }
164
195
  }
165
196
  return chips;