@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.
- package/CHANGELOG.md +37 -0
- package/README.md +11 -7
- package/dist/index.d.ts +55 -0
- package/dist/opengridx.es.js +6346 -6279
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +4 -0
- package/docs/architecture/datagrid-orchestration.md +4 -2
- package/docs/architecture/use-grid-virtualization.md +2 -0
- package/docs/components/pagination.md +1 -1
- package/docs/features/ai-toolkit.md +1 -0
- package/docs/features/rtl.md +1 -1
- package/docs/performance.md +19 -1
- package/docs/roadmap.md +26 -5
- package/lib/components/Cell/Cell.tsx +2 -1
- package/lib/components/DataGrid/DataGrid.tsx +19 -18
- package/lib/components/DataGrid/GridListView.tsx +1 -1
- package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
- package/lib/components/DataGrid/GridViewport.tsx +2 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
- package/lib/components/Header/Header.tsx +4 -3
- package/lib/components/Row/Row.tsx +6 -2
- package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
- package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
- package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
- package/lib/components/Toolbar/GridToolbar.tsx +7 -380
- package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
- package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
- package/lib/hooks/core/gridKeyDown.ts +272 -0
- package/lib/hooks/core/useDataGrid.ts +13 -5
- package/lib/hooks/core/useGridApiMethods.ts +2 -2
- package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
- package/lib/hooks/core/useGridCellSelection.ts +17 -297
- package/lib/hooks/core/useGridColumnLookup.ts +2 -1
- package/lib/hooks/core/useGridColumns.tsx +3 -2
- package/lib/hooks/core/useGridControlledState.ts +3 -2
- package/lib/hooks/core/useGridHierarchy.ts +1 -1
- package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
- package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
- package/lib/hooks/core/useGridRowInteractions.ts +1 -1
- package/lib/hooks/core/useGridRowSelection.ts +11 -2
- package/lib/hooks/core/useGridToolbarProps.ts +3 -2
- package/lib/hooks/features/useGridAiAssistant.ts +3 -2
- package/lib/hooks/useColumnReorder.ts +2 -1
- package/lib/hooks/useTreeData.ts +4 -2
- package/lib/types/locale.ts +22 -0
- package/lib/utils/aggregation/index.ts +16 -0
- package/lib/utils/aiAssistant.ts +46 -15
- package/lib/utils/export/excelCells.ts +244 -0
- package/lib/utils/export/exportCsv.ts +179 -0
- package/lib/utils/export/exportExcelHtml.ts +220 -0
- package/lib/utils/export/exportJson.ts +179 -0
- package/lib/utils/export/exportRows.ts +85 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
- package/lib/utils/export/index.ts +5 -695
- package/lib/utils/export/printGrid.ts +282 -0
- package/lib/utils/gridLocale.ts +30 -0
- package/lib/utils/selectionSets.ts +36 -0
- package/lib/utils/untypedColumns.ts +16 -0
- package/llms.txt +3 -1
- package/package.json +6 -2
- package/lib/theme/__baseline__/default-theme.darwin.chromium.json +0 -1
- package/lib/theme/__baseline__/default-theme.darwin.firefox.json +0 -1
- package/lib/theme/__baseline__/default-theme.darwin.webkit.json +0 -1
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
19
|
-
|
|
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:
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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:
|
|
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):
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
83
|
-
baseColumns: baseColumns
|
|
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
|
|
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]
|
|
93
|
+
column: untypedColumn(columns[oldIndex]),
|
|
93
94
|
oldIndex,
|
|
94
95
|
targetIndex,
|
|
95
96
|
});
|
package/lib/hooks/useTreeData.ts
CHANGED
|
@@ -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
|
-
|
|
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]);
|
package/lib/types/locale.ts
CHANGED
|
@@ -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
|
+
}
|
package/lib/utils/aiAssistant.ts
CHANGED
|
@@ -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:
|
|
138
|
-
if (items.length === 0 && words.length === 0) chips.push({ id: 'filter:clear', part: 'filter', label:
|
|
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:
|
|
142
|
-
if (sortModel.length === 0) chips.push({ id: 'sort:clear', part: 'sort', label:
|
|
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:
|
|
146
|
-
if (rowGroupingModel.length === 0) chips.push({ id: 'grouping:clear', part: 'grouping', label:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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:
|
|
193
|
+
chips.push({ id: `columnVisibility:${field}`, part: 'columnVisibility', label: texts.visibility(label(field), Boolean(columnVisibilityModel[field])) });
|
|
163
194
|
}
|
|
164
195
|
}
|
|
165
196
|
return chips;
|