@opencorestack/opengridx 2.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -161
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +45 -22
- package/docs/components/datagrid.md +14 -12
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +100 -36
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +147 -41
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +15 -3
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +284 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +123 -0
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +20 -7
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +86 -90
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +78 -0
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -137
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { getAriaRowLayout, type GridAriaRowLayout } from '../../utils/aria';
|
|
3
|
+
import { getColumnGroupDepth } from '../../utils/columnGroups';
|
|
4
|
+
import type { GridColumnGroupingModel, GridPaginationModel } from '../../types';
|
|
5
|
+
|
|
6
|
+
export interface UseGridAriaRowsParams {
|
|
7
|
+
columnGroupingModel: GridColumnGroupingModel | undefined;
|
|
8
|
+
pinnedTopCount: number;
|
|
9
|
+
pinnedBottomCount: number;
|
|
10
|
+
pagination: boolean;
|
|
11
|
+
paginationMode: 'client' | 'server' | 'infinite';
|
|
12
|
+
paginationModel: GridPaginationModel;
|
|
13
|
+
/** What the pager pages through (see `getPaginationRowCount`). */
|
|
14
|
+
paginationRowCount: number;
|
|
15
|
+
paginatedUnpinnedRowCount: number;
|
|
16
|
+
sortedUnpinnedRowCount: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `aria-rowcount` and the `aria-rowindex` bases of the pinned and centre rows. */
|
|
20
|
+
export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayout {
|
|
21
|
+
const {
|
|
22
|
+
columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
|
|
23
|
+
paginationRowCount, paginatedUnpinnedRowCount, sortedUnpinnedRowCount,
|
|
24
|
+
} = params;
|
|
25
|
+
const { page, pageSize } = paginationModel;
|
|
26
|
+
|
|
27
|
+
return useMemo(() => getAriaRowLayout({
|
|
28
|
+
headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel),
|
|
29
|
+
pinnedTopCount,
|
|
30
|
+
pinnedBottomCount,
|
|
31
|
+
pageRowCount: pagination ? paginatedUnpinnedRowCount : sortedUnpinnedRowCount,
|
|
32
|
+
totalCenterRowCount: pagination && paginationMode === 'server'
|
|
33
|
+
? Math.max(paginationRowCount, paginatedUnpinnedRowCount)
|
|
34
|
+
: sortedUnpinnedRowCount,
|
|
35
|
+
pageOffset: pagination ? page * pageSize : 0,
|
|
36
|
+
}), [columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
|
|
37
|
+
sortedUnpinnedRowCount, paginationMode, paginationRowCount, page, pageSize]);
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect } from 'react';
|
|
2
|
+
import type { MutableRefObject, RefObject } from 'react';
|
|
3
|
+
import { useGridClipboard } from '../features/useGridClipboard';
|
|
4
|
+
import type { GridApi, GridColDef, GridRowId, GridRowModel } from '../../types';
|
|
5
|
+
|
|
6
|
+
export interface UseGridClipboardApiParams {
|
|
7
|
+
apiRef: MutableRefObject<GridApi>;
|
|
8
|
+
/** The rendered data columns, in screen order. */
|
|
9
|
+
columns: GridColDef[];
|
|
10
|
+
getRowId: (row: GridRowModel) => GridRowId;
|
|
11
|
+
/** The grid root: Ctrl/Cmd+C only copies while focus is inside it. */
|
|
12
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
13
|
+
disableKeyboardShortcut: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Copies the selected rows (every page, collapsed groups, pinned rows) with the on-screen columns
|
|
18
|
+
* in screen order, on Ctrl/Cmd+C while focus is inside this grid, and installs `copySelectedRows`
|
|
19
|
+
* on the API for programmatic use.
|
|
20
|
+
*/
|
|
21
|
+
export function useGridClipboardApi(params: UseGridClipboardApiParams): void {
|
|
22
|
+
const { apiRef, columns, getRowId, containerRef, disableKeyboardShortcut } = params;
|
|
23
|
+
|
|
24
|
+
const getGridRootElement = useCallback(() => containerRef.current, [containerRef]);
|
|
25
|
+
const { copySelectedRows } = useGridClipboard({
|
|
26
|
+
getSelectedRowIds: () => apiRef.current.getSelectedRows(),
|
|
27
|
+
getColumns: () => columns,
|
|
28
|
+
getRows: () => apiRef.current.getAllFilteredRows(),
|
|
29
|
+
getRowId,
|
|
30
|
+
getRootElement: getGridRootElement,
|
|
31
|
+
disableKeyboardShortcut,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Stable identity.
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
apiRef.current.copySelectedRows = copySelectedRows;
|
|
37
|
+
}, [copySelectedRows, apiRef]);
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { GridColDef, GridRowModel } from '../../types';
|
|
3
|
+
import { buildColumnLookup, type GridColumnLookup } from '../../utils/columnLookup';
|
|
4
|
+
|
|
5
|
+
const SEPARATOR = '\u0000';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The column lookup used by client-side filtering, sorting and the quick filter.
|
|
9
|
+
*
|
|
10
|
+
* Its identity changes only when the columns change or the set of hidden columns changes, so an
|
|
11
|
+
* inline `columnVisibilityModel` object (a new reference on every parent render) does not re-run
|
|
12
|
+
* the filter and sort passes.
|
|
13
|
+
*/
|
|
14
|
+
export function useGridColumnLookup<R extends GridRowModel>(
|
|
15
|
+
columns: GridColDef<R>[],
|
|
16
|
+
columnVisibilityModel: Record<string, boolean>,
|
|
17
|
+
): GridColumnLookup {
|
|
18
|
+
const hiddenFieldsKey = columns
|
|
19
|
+
.filter(col => columnVisibilityModel[col.field] === false)
|
|
20
|
+
.map(col => col.field)
|
|
21
|
+
.join(SEPARATOR);
|
|
22
|
+
|
|
23
|
+
return useMemo(() => {
|
|
24
|
+
const hidden: Record<string, boolean> = {};
|
|
25
|
+
if (hiddenFieldsKey) {
|
|
26
|
+
for (const field of hiddenFieldsKey.split(SEPARATOR)) hidden[field] = false;
|
|
27
|
+
}
|
|
28
|
+
return buildColumnLookup(columns as unknown as GridColDef[], hidden);
|
|
29
|
+
}, [columns, hiddenFieldsKey]);
|
|
30
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
2
2
|
import { useColumnReorder } from '../useColumnReorder';
|
|
3
3
|
import { ExpandIcon } from '../../components/ui/ExpandIcon';
|
|
4
|
+
import { isColumnPinned } from '../../utils/pinning';
|
|
5
|
+
import { formatAggregateForColumn } from '../../utils/aggregation';
|
|
6
|
+
import { CellErrorBoundary } from '../../components/Cell/CellErrorBoundary';
|
|
7
|
+
import { CHECKBOX_FIELD, EXPAND_FIELD, REORDER_FIELD } from '../../utils/focus';
|
|
4
8
|
import type {
|
|
5
9
|
GridColDef,
|
|
6
10
|
GridRowModel,
|
|
7
11
|
GridRowId,
|
|
8
12
|
GridColumnOrderChangeParams,
|
|
13
|
+
GridColumnPinning,
|
|
9
14
|
GridRenderCellParams,
|
|
15
|
+
GridAggregationModel,
|
|
10
16
|
} from '../../types';
|
|
11
17
|
import type { GridInitialState } from '../../state/types';
|
|
12
18
|
|
|
@@ -14,6 +20,119 @@ interface HierarchyHandlers {
|
|
|
14
20
|
toggleExpansion: (id: GridRowId) => void;
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
// A group row carries each column's aggregate in the column's field. It is shown like the footer
|
|
24
|
+
// shows totals: the stored aggregate (a valueGetter would recompute it from fields a group row does
|
|
25
|
+
// not have), formatted with formatAggregateForColumn (so a count is never put in a currency format).
|
|
26
|
+
function withGroupRowAggregates<R extends GridRowModel>(
|
|
27
|
+
col: GridColDef<R>,
|
|
28
|
+
fnName: string | undefined,
|
|
29
|
+
groupingRows: ReadonlyMap<GridRowId, unknown> | undefined,
|
|
30
|
+
): Pick<GridColDef<R>, 'valueGetter' | 'valueFormatter'> {
|
|
31
|
+
if (!fnName || !groupingRows) return { valueGetter: col.valueGetter, valueFormatter: col.valueFormatter };
|
|
32
|
+
const isGroupRow = (row: R) => groupingRows.has(row.id);
|
|
33
|
+
const ownGetter = col.valueGetter;
|
|
34
|
+
const ownFormatter = col.valueFormatter;
|
|
35
|
+
return {
|
|
36
|
+
valueGetter: ownGetter
|
|
37
|
+
? (p) => (isGroupRow(p.row) ? p.value : ownGetter(p))
|
|
38
|
+
: undefined,
|
|
39
|
+
valueFormatter: (p) => {
|
|
40
|
+
if (isGroupRow(p.row)) return formatAggregateForColumn(p.value, fnName, col, p.row);
|
|
41
|
+
if (ownFormatter) return ownFormatter(p);
|
|
42
|
+
return p.value == null ? '' : String(p.value);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Columns in render order: left-pinned in `pinnedColumns.left` order, unpinned in column order,
|
|
49
|
+
* right-pinned in `pinnedColumns.right` order — the order useLayout renders them in.
|
|
50
|
+
*/
|
|
51
|
+
function inPinnedRenderOrder<C extends { field: string }>(columns: C[], pinnedColumns?: GridColumnPinning): C[] {
|
|
52
|
+
if (!pinnedColumns) return columns;
|
|
53
|
+
const left = pinnedColumns.left ?? [];
|
|
54
|
+
const right = pinnedColumns.right ?? [];
|
|
55
|
+
const rank = (col: C) => {
|
|
56
|
+
const side = isColumnPinned(col.field, pinnedColumns);
|
|
57
|
+
if (side === 'left') return left.indexOf(col.field) - left.length;
|
|
58
|
+
if (side === 'right') return 1 + right.indexOf(col.field);
|
|
59
|
+
return 0;
|
|
60
|
+
};
|
|
61
|
+
return [...columns].sort((a, b) => rank(a) - rank(b));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Injected hierarchy renderers replace the plain-cell path in Cell, so they must
|
|
65
|
+
// render the formatted value themselves or valueFormatter is silently lost.
|
|
66
|
+
const displayValue = <R extends GridRowModel>(cellParams: GridRenderCellParams<R>): React.ReactNode =>
|
|
67
|
+
cellParams.formattedValue ?? (cellParams.value as React.ReactNode);
|
|
68
|
+
|
|
69
|
+
interface ConsumerCellResult {
|
|
70
|
+
useDefault: boolean;
|
|
71
|
+
content?: React.ReactNode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Calls the consumer's renderCell for a hierarchy column cell. For a synthetic row (group, subtotal or
|
|
76
|
+
* auto-created tree parent) a result of `undefined` means "use the grid's default content". In the
|
|
77
|
+
* hierarchy column the output is wrapped in its own error boundary, so a renderCell that fails on a
|
|
78
|
+
* group row shows the error icon next to the expand toggle instead of replacing the toggle.
|
|
79
|
+
*/
|
|
80
|
+
function renderConsumerCell<R extends GridRowModel>(
|
|
81
|
+
col: GridColDef<R>,
|
|
82
|
+
cellParams: GridRenderCellParams<R>,
|
|
83
|
+
isolate: boolean,
|
|
84
|
+
): ConsumerCellResult {
|
|
85
|
+
const renderCell = col.renderCell;
|
|
86
|
+
if (!renderCell) return { useDefault: true };
|
|
87
|
+
const isSynthetic = Boolean(cellParams.rowMeta?.isGroupRow);
|
|
88
|
+
if (!isolate) {
|
|
89
|
+
const output = renderCell(cellParams);
|
|
90
|
+
return isSynthetic && output === undefined ? { useDefault: true } : { useDefault: false, content: output };
|
|
91
|
+
}
|
|
92
|
+
let output: React.ReactNode;
|
|
93
|
+
try {
|
|
94
|
+
output = renderCell(cellParams);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
useDefault: false,
|
|
98
|
+
content: <CellErrorBoundary field={col.field} resetKey={cellParams.row} renderFn={() => { throw error; }} />,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (isSynthetic && output === undefined) return { useDefault: true };
|
|
102
|
+
return { useDefault: false, content: <CellErrorBoundary field={col.field} resetKey={cellParams.row} renderFn={() => output} /> };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** What the hierarchy column shows without a consumer renderCell (or when it returns undefined for a synthetic row). */
|
|
106
|
+
function defaultHierarchyContent<R extends GridRowModel>(
|
|
107
|
+
cellParams: GridRenderCellParams<R>,
|
|
108
|
+
isRowGrouping: boolean,
|
|
109
|
+
isTreeData: boolean,
|
|
110
|
+
): React.ReactNode {
|
|
111
|
+
const meta = cellParams.rowMeta;
|
|
112
|
+
if (!meta?.isGroupRow) return displayValue(cellParams);
|
|
113
|
+
// A subtotal row carries only aggregates; its label column stays empty.
|
|
114
|
+
if (meta.isGroupFooter) return null;
|
|
115
|
+
const descendantCount = meta.descendantCount;
|
|
116
|
+
if (isRowGrouping && meta.hasChildren) {
|
|
117
|
+
const groupLabel = meta.groupLabel ?? `${meta.groupingField}: ${String(meta.groupingValue)}`;
|
|
118
|
+
return (
|
|
119
|
+
<div className="ogx__group-cell-content">
|
|
120
|
+
{groupLabel}
|
|
121
|
+
{descendantCount !== undefined ? ` (${descendantCount})` : ''}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (isTreeData) {
|
|
126
|
+
return (
|
|
127
|
+
<div className="ogx__group-cell-content">
|
|
128
|
+
{meta.groupLabel}
|
|
129
|
+
{descendantCount !== undefined && descendantCount > 0 ? ` (${descendantCount})` : ''}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return displayValue(cellParams);
|
|
134
|
+
}
|
|
135
|
+
|
|
17
136
|
export interface UseGridColumnsParams<R extends GridRowModel> {
|
|
18
137
|
activeColumns: GridColDef<R>[];
|
|
19
138
|
isHierarchyEnabled: boolean;
|
|
@@ -23,6 +142,8 @@ export interface UseGridColumnsParams<R extends GridRowModel> {
|
|
|
23
142
|
columnVisibilityModel: Record<string, boolean>;
|
|
24
143
|
columnOrder?: string[];
|
|
25
144
|
onColumnOrderChange?: (params: GridColumnOrderChangeParams) => void;
|
|
145
|
+
/** Receives the whole new column order after every reorder and reset (not in pivot mode). */
|
|
146
|
+
onColumnOrderModelChange?: (columnOrder: string[]) => void;
|
|
26
147
|
disableColumnReorder: boolean;
|
|
27
148
|
pivotMode: boolean;
|
|
28
149
|
checkboxSelection: boolean;
|
|
@@ -30,17 +151,37 @@ export interface UseGridColumnsParams<R extends GridRowModel> {
|
|
|
30
151
|
rowReordering: boolean;
|
|
31
152
|
initialState?: GridInitialState;
|
|
32
153
|
setColumns: (cols: GridColDef[]) => void;
|
|
154
|
+
pinnedColumns?: GridColumnPinning;
|
|
155
|
+
/** Resolves a row's id (getRowId) when the injected expand toggle is clicked; defaults to `row.id`. */
|
|
156
|
+
getRowId?: (row: R) => GridRowId;
|
|
157
|
+
/** Formats the aggregates on row-grouping group rows. */
|
|
158
|
+
aggregationModel?: GridAggregationModel;
|
|
159
|
+
/** Row grouping only: the synthetic group rows, keyed by id. */
|
|
160
|
+
groupingRows?: ReadonlyMap<GridRowId, unknown>;
|
|
33
161
|
}
|
|
34
162
|
|
|
163
|
+
const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
|
|
164
|
+
|
|
35
165
|
export interface UseGridColumnsResult<R extends GridRowModel> {
|
|
36
166
|
effectiveColumns: GridColDef<R>[];
|
|
167
|
+
/** Tree data / row grouping: the column that shows the expand toggle, indent and group labels. */
|
|
168
|
+
hierarchyField: string | undefined;
|
|
37
169
|
orderedColumns: GridColDef<R>[];
|
|
38
170
|
visibleOrderedColumns: GridColDef<R>[];
|
|
39
|
-
|
|
171
|
+
/** Visible data columns in render order: left-pinned, unpinned, right-pinned (`apiRef.getVisibleColumns`). */
|
|
172
|
+
renderOrderedColumns: GridColDef<R>[];
|
|
173
|
+
/** Focusable columns in render order: system columns, then the visible data columns (left-pinned, unpinned, right-pinned). */
|
|
174
|
+
navigationColumns: Array<GridColDef<R> | { field: string; sortable: false; editable: false }>;
|
|
175
|
+
/** Position of each visible data column in render order (system columns excluded): the public `colIndex`. */
|
|
176
|
+
columnIndexMap: Map<string, number>;
|
|
40
177
|
columnWidths: Record<string, number>;
|
|
41
178
|
effectiveColumnOrder: string[];
|
|
42
179
|
setInternalColumnOrder: React.Dispatch<React.SetStateAction<string[]>>;
|
|
43
180
|
columnReorderHandlers: ReturnType<typeof useColumnReorder>;
|
|
181
|
+
/** Moves `fromField` to the position of `toField` in the column order (Columns panel drag). */
|
|
182
|
+
moveColumn: (fromField: string, toField: string) => void;
|
|
183
|
+
/** Restores the columns' definition order. */
|
|
184
|
+
resetColumnOrder: () => void;
|
|
44
185
|
handleColumnResize: (field: string, newWidth: number) => void;
|
|
45
186
|
}
|
|
46
187
|
|
|
@@ -56,6 +197,7 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
56
197
|
columnVisibilityModel,
|
|
57
198
|
columnOrder,
|
|
58
199
|
onColumnOrderChange,
|
|
200
|
+
onColumnOrderModelChange,
|
|
59
201
|
disableColumnReorder,
|
|
60
202
|
pivotMode,
|
|
61
203
|
checkboxSelection,
|
|
@@ -63,59 +205,95 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
63
205
|
rowReordering,
|
|
64
206
|
initialState,
|
|
65
207
|
setColumns,
|
|
208
|
+
pinnedColumns,
|
|
209
|
+
getRowId = defaultGetRowId,
|
|
210
|
+
aggregationModel,
|
|
211
|
+
groupingRows,
|
|
66
212
|
} = params;
|
|
67
213
|
|
|
214
|
+
// ── Column order ──────────────────────────────────────────────────────────
|
|
215
|
+
const naturalOrder = useMemo(() => activeColumns.map(col => col.field), [activeColumns]);
|
|
216
|
+
|
|
217
|
+
// null = the columns' own order, until the user reorders. Deriving it (instead of snapshotting the
|
|
218
|
+
// columns at mount) keeps it right when the grid mounts in pivot mode or the columns change.
|
|
219
|
+
const [storedColumnOrder, setStoredColumnOrder] = useState<string[] | null>(
|
|
220
|
+
() => initialState?.columns?.columnOrder ?? null
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Generated pivot columns keep an order of their own, so pivoting never rewrites the user's column
|
|
224
|
+
// order, and a controlled `columnOrder` (which names source columns) does not apply to them. It
|
|
225
|
+
// resets whenever the generated column set changes.
|
|
226
|
+
const pivotColumnsKey = pivotMode ? naturalOrder.join('\u0000') : '';
|
|
227
|
+
const [pivotOrderState, setPivotOrderState] = useState<{ key: string; order: string[] } | null>(null);
|
|
228
|
+
const pivotColumnOrder = pivotOrderState && pivotOrderState.key === pivotColumnsKey ? pivotOrderState.order : naturalOrder;
|
|
229
|
+
|
|
230
|
+
const internalColumnOrder = storedColumnOrder ?? naturalOrder;
|
|
231
|
+
const effectiveColumnOrder = pivotMode ? pivotColumnOrder : (columnOrder ?? internalColumnOrder);
|
|
232
|
+
|
|
233
|
+
const setInternalColumnOrder = useCallback<React.Dispatch<React.SetStateAction<string[]>>>((action) => {
|
|
234
|
+
if (pivotMode) {
|
|
235
|
+
setPivotOrderState(prev => {
|
|
236
|
+
const base = prev && prev.key === pivotColumnsKey ? prev.order : naturalOrder;
|
|
237
|
+
return { key: pivotColumnsKey, order: typeof action === 'function' ? action(base) : action };
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
setStoredColumnOrder(prev => (typeof action === 'function' ? action(prev ?? naturalOrder) : action));
|
|
242
|
+
}, [pivotMode, pivotColumnsKey, naturalOrder]);
|
|
243
|
+
|
|
244
|
+
// In pivot mode the row-label columns (marked hideable: false) always show: the pivot rows are
|
|
245
|
+
// labelled by them, and they share their field with the source column a visibility model may hide.
|
|
246
|
+
const isColumnShown = useCallback(
|
|
247
|
+
(col: GridColDef<R>) => (pivotMode && col.hideable === false) || columnVisibilityModel[col.field] !== false,
|
|
248
|
+
[pivotMode, columnVisibilityModel],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// The expand toggle, indentation and group label go on the leftmost column actually
|
|
252
|
+
// on screen, so hiding, reordering or pinning columns never strips group rows of them.
|
|
253
|
+
// Mirrors the render order: left-pinned, then unpinned, then right-pinned.
|
|
254
|
+
const hierarchyField = useMemo<string | undefined>(() => {
|
|
255
|
+
if (!isHierarchyEnabled) return undefined;
|
|
256
|
+
const orderIndex = new Map(effectiveColumnOrder.map((field, idx) => [field, idx]));
|
|
257
|
+
const rank = (col: GridColDef<R>) => orderIndex.get(col.field) ?? activeColumns.indexOf(col);
|
|
258
|
+
const ordered = [...activeColumns].sort((a, b) => rank(a) - rank(b));
|
|
259
|
+
const onScreen = inPinnedRenderOrder(ordered.filter(col => columnVisibilityModel[col.field] !== false), pinnedColumns);
|
|
260
|
+
return (onScreen[0] ?? activeColumns[0])?.field;
|
|
261
|
+
}, [isHierarchyEnabled, effectiveColumnOrder, activeColumns, columnVisibilityModel, pinnedColumns]);
|
|
262
|
+
|
|
68
263
|
// ── Effective columns (hierarchy cell renderer injection) ─────────────────
|
|
69
264
|
const effectiveColumns = useMemo<GridColDef<R>[]>(() => {
|
|
70
265
|
if (!isHierarchyEnabled) return activeColumns;
|
|
71
266
|
|
|
72
|
-
|
|
73
|
-
|
|
267
|
+
const groupRows = isRowGrouping ? groupingRows : undefined;
|
|
268
|
+
|
|
269
|
+
return activeColumns.map((col) => {
|
|
270
|
+
const groupRowAggregates = withGroupRowAggregates(col, aggregationModel?.[col.field], groupRows);
|
|
271
|
+
if (col.field === hierarchyField) {
|
|
74
272
|
return {
|
|
75
273
|
...col,
|
|
274
|
+
...groupRowAggregates,
|
|
76
275
|
renderCell: (cellParams: GridRenderCellParams<R>) => {
|
|
77
276
|
const meta = cellParams.rowMeta;
|
|
78
277
|
const depth = meta?.treeDepth ?? 0;
|
|
79
278
|
const hasChildren = Boolean(meta?.hasChildren);
|
|
80
279
|
const isExpanded = Boolean(meta?.isExpanded);
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const isGroupRow = Boolean(meta?.isGroupRow);
|
|
85
|
-
|
|
86
|
-
let content: React.ReactNode = col.renderCell ? col.renderCell(cellParams) : cellParams.value as React.ReactNode;
|
|
87
|
-
|
|
88
|
-
if (isTreeData && hasChildren && isGroupRow) {
|
|
89
|
-
content = (
|
|
90
|
-
<div className="ogx__group-cell-content">
|
|
91
|
-
{cellParams.value as React.ReactNode}
|
|
92
|
-
{descendantCount !== undefined && descendantCount > 0 ? ` (${descendantCount})` : ''}
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (isRowGrouping && hasChildren && groupingField) {
|
|
98
|
-
const groupLabel = meta?.groupLabel ?? `${groupingField}: ${String(groupingValue)}`;
|
|
99
|
-
content = (
|
|
100
|
-
<div className="ogx__group-cell-content">
|
|
101
|
-
{groupLabel}
|
|
102
|
-
{descendantCount !== undefined ? ` (${descendantCount})` : ''}
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
280
|
+
|
|
281
|
+
const custom = renderConsumerCell(col, cellParams, true);
|
|
282
|
+
const content = custom.useDefault ? defaultHierarchyContent(cellParams, isRowGrouping, isTreeData) : custom.content;
|
|
106
283
|
|
|
107
284
|
return (
|
|
108
285
|
<div style={{ display: 'flex', alignItems: 'center', paddingLeft: depth * 24, width: '100%', height: '100%' }}>
|
|
109
286
|
<div style={{ marginRight: 4, width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: hasChildren ? 'pointer' : 'default', flexShrink: 0 }}>
|
|
110
287
|
{hasChildren ? (
|
|
111
288
|
<div
|
|
112
|
-
onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(cellParams.row
|
|
289
|
+
onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(getRowId(cellParams.row)); }}
|
|
113
290
|
onMouseDown={(e) => { e.stopPropagation(); }}
|
|
114
291
|
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'auto', zIndex: 10, position: 'relative' }}
|
|
115
292
|
>
|
|
116
293
|
<ExpandIcon
|
|
117
294
|
isExpanded={isExpanded}
|
|
118
|
-
|
|
295
|
+
tabIndex={-1}
|
|
296
|
+
onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(getRowId(cellParams.row)); }}
|
|
119
297
|
/>
|
|
120
298
|
</div>
|
|
121
299
|
) : null}
|
|
@@ -129,24 +307,22 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
129
307
|
|
|
130
308
|
return {
|
|
131
309
|
...col,
|
|
310
|
+
...groupRowAggregates,
|
|
132
311
|
renderCell: (cellParams: GridRenderCellParams<R>) => {
|
|
133
312
|
const meta = cellParams.rowMeta;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (isRowGrouping && hasChildren) {
|
|
138
|
-
if (col.field === groupingField) return null;
|
|
139
|
-
if (cellParams.value !== undefined && cellParams.value !== null) {
|
|
140
|
-
return col.renderCell ? col.renderCell(cellParams) : cellParams.value;
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
313
|
+
if (!meta?.isGroupRow) {
|
|
314
|
+
return col.renderCell ? col.renderCell(cellParams) : displayValue(cellParams);
|
|
143
315
|
}
|
|
144
|
-
|
|
145
|
-
|
|
316
|
+
// Synthetic rows: the consumer's renderCell decides; `undefined` keeps the default,
|
|
317
|
+
// which is the aggregate (or nothing) and never the grouping value the label already shows.
|
|
318
|
+
const custom = renderConsumerCell(col, cellParams, false);
|
|
319
|
+
if (!custom.useDefault) return custom.content;
|
|
320
|
+
if (col.field === meta.groupingField && !meta.isGroupFooter) return null;
|
|
321
|
+
return cellParams.value == null ? null : displayValue(cellParams);
|
|
146
322
|
}
|
|
147
323
|
};
|
|
148
324
|
}) as GridColDef<R>[];
|
|
149
|
-
}, [activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers]);
|
|
325
|
+
}, [activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers, hierarchyField, getRowId, aggregationModel, groupingRows]);
|
|
150
326
|
|
|
151
327
|
// ── Column widths ─────────────────────────────────────────────────────────
|
|
152
328
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
|
|
@@ -157,72 +333,104 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
157
333
|
setColumnWidths(prev => ({ ...prev, [field]: newWidth }));
|
|
158
334
|
}, []);
|
|
159
335
|
|
|
160
|
-
// ── Column order ──────────────────────────────────────────────────────────
|
|
161
|
-
const [internalColumnOrder, setInternalColumnOrder] = useState<string[]>(
|
|
162
|
-
() => initialState?.columns?.columnOrder ?? activeColumns.map(col => col.field)
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
const effectiveColumnOrder = columnOrder ?? internalColumnOrder;
|
|
166
|
-
|
|
167
336
|
useEffect(() => {
|
|
168
337
|
setColumns(activeColumns as unknown as GridColDef[]);
|
|
169
338
|
}, [activeColumns, setColumns]);
|
|
170
339
|
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
if (pivotMode) {
|
|
173
|
-
setInternalColumnOrder(activeColumns.map(col => col.field));
|
|
174
|
-
}
|
|
175
|
-
}, [pivotMode, activeColumns]);
|
|
176
|
-
|
|
177
340
|
// ── Ordered / visible columns ─────────────────────────────────────────────
|
|
341
|
+
// A controlled or initial column order always applies. disableColumnReorder only removes the
|
|
342
|
+
// user's ways to change it (header drag, Columns-panel reorder).
|
|
178
343
|
const orderedColumns = useMemo<GridColDef<R>[]>(() => {
|
|
179
|
-
if (disableColumnReorder) return effectiveColumns;
|
|
180
|
-
|
|
181
344
|
const orderMap = new Map(effectiveColumnOrder.map((field, idx) => [field, idx]));
|
|
182
345
|
return [...effectiveColumns].sort((a, b) => {
|
|
183
346
|
const ai = orderMap.get(a.field) ?? effectiveColumns.indexOf(a);
|
|
184
347
|
const bi = orderMap.get(b.field) ?? effectiveColumns.indexOf(b);
|
|
185
348
|
return ai - bi;
|
|
186
349
|
});
|
|
187
|
-
}, [effectiveColumns, effectiveColumnOrder
|
|
350
|
+
}, [effectiveColumns, effectiveColumnOrder]);
|
|
188
351
|
|
|
189
352
|
const visibleOrderedColumns = useMemo<GridColDef<R>[]>(
|
|
190
|
-
() => orderedColumns.filter(
|
|
191
|
-
[orderedColumns,
|
|
353
|
+
() => orderedColumns.filter(isColumnShown),
|
|
354
|
+
[orderedColumns, isColumnShown]
|
|
192
355
|
);
|
|
193
356
|
|
|
194
|
-
// ── Column reorder
|
|
357
|
+
// ── Column reorder ────────────────────────────────────────────────────────
|
|
358
|
+
// Every move works on the full current order (orderedColumns: all columns, including ones added
|
|
359
|
+
// after mount or missing from a stored/controlled order, and the synthetic __group__ column),
|
|
360
|
+
// so the reported indices and the stored order always describe the same list.
|
|
361
|
+
const commitColumnOrder = useCallback((newOrder: string[], change?: GridColumnOrderChangeParams) => {
|
|
362
|
+
// A controlled columnOrder names source columns; generated pivot columns keep their own order.
|
|
363
|
+
if (pivotMode || !columnOrder) setInternalColumnOrder(newOrder);
|
|
364
|
+
if (change) onColumnOrderChange?.(change);
|
|
365
|
+
if (!pivotMode) onColumnOrderModelChange?.(newOrder);
|
|
366
|
+
}, [pivotMode, columnOrder, setInternalColumnOrder, onColumnOrderChange, onColumnOrderModelChange]);
|
|
367
|
+
|
|
368
|
+
const applyColumnMove = useCallback((oldIndex: number, targetIndex: number) => {
|
|
369
|
+
const newOrder = orderedColumns.map(col => col.field);
|
|
370
|
+
const [movedField] = newOrder.splice(oldIndex, 1);
|
|
371
|
+
newOrder.splice(targetIndex, 0, movedField);
|
|
372
|
+
commitColumnOrder(newOrder, {
|
|
373
|
+
column: orderedColumns[oldIndex] as unknown as GridColDef,
|
|
374
|
+
oldIndex,
|
|
375
|
+
targetIndex,
|
|
376
|
+
});
|
|
377
|
+
}, [orderedColumns, commitColumnOrder]);
|
|
378
|
+
|
|
195
379
|
const columnReorderHandlers = useColumnReorder({
|
|
196
380
|
columns: orderedColumns,
|
|
197
|
-
onColumnOrderChange: useCallback(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
newOrder.splice(targetIndex, 0, movedField);
|
|
202
|
-
if (!columnOrder) setInternalColumnOrder(newOrder);
|
|
203
|
-
onColumnOrderChange?.(reorderParams);
|
|
204
|
-
}, [effectiveColumnOrder, columnOrder, onColumnOrderChange]),
|
|
381
|
+
onColumnOrderChange: useCallback(
|
|
382
|
+
({ oldIndex, targetIndex }: GridColumnOrderChangeParams) => applyColumnMove(oldIndex, targetIndex),
|
|
383
|
+
[applyColumnMove]
|
|
384
|
+
),
|
|
205
385
|
disableColumnReorder,
|
|
206
386
|
});
|
|
207
387
|
|
|
208
|
-
|
|
388
|
+
const moveColumn = useCallback((fromField: string, toField: string) => {
|
|
389
|
+
const oldIndex = orderedColumns.findIndex(col => col.field === fromField);
|
|
390
|
+
const targetIndex = orderedColumns.findIndex(col => col.field === toField);
|
|
391
|
+
if (oldIndex === -1 || targetIndex === -1 || oldIndex === targetIndex) return;
|
|
392
|
+
applyColumnMove(oldIndex, targetIndex);
|
|
393
|
+
}, [orderedColumns, applyColumnMove]);
|
|
394
|
+
|
|
395
|
+
const resetColumnOrder = useCallback(() => {
|
|
396
|
+
commitColumnOrder(naturalOrder);
|
|
397
|
+
}, [commitColumnOrder, naturalOrder]);
|
|
398
|
+
|
|
399
|
+
// ── Navigation columns (system cols + rendered data cols, in render order) ─
|
|
400
|
+
// Mirrors what Row and Header render, so arrow keys follow the screen and never
|
|
401
|
+
// land on a hidden column. System columns are never sortable or editable.
|
|
402
|
+
const renderedDataColumns = useMemo(
|
|
403
|
+
() => inPinnedRenderOrder(visibleOrderedColumns, pinnedColumns),
|
|
404
|
+
[visibleOrderedColumns, pinnedColumns]
|
|
405
|
+
);
|
|
406
|
+
|
|
209
407
|
const navigationColumns = useMemo(() => {
|
|
210
|
-
const specials: { field: string }[] = [];
|
|
211
|
-
if (rowReordering) specials.push({ field:
|
|
212
|
-
if (hasDetailPanel) specials.push({ field:
|
|
213
|
-
if (checkboxSelection) specials.push({ field:
|
|
214
|
-
return [...specials, ...
|
|
215
|
-
}, [
|
|
408
|
+
const specials: { field: string; sortable: false; editable: false }[] = [];
|
|
409
|
+
if (rowReordering) specials.push({ field: REORDER_FIELD, sortable: false, editable: false });
|
|
410
|
+
if (hasDetailPanel) specials.push({ field: EXPAND_FIELD, sortable: false, editable: false });
|
|
411
|
+
if (checkboxSelection) specials.push({ field: CHECKBOX_FIELD, sortable: false, editable: false });
|
|
412
|
+
return [...specials, ...renderedDataColumns];
|
|
413
|
+
}, [renderedDataColumns, checkboxSelection, hasDetailPanel, rowReordering]);
|
|
414
|
+
|
|
415
|
+
const columnIndexMap = useMemo(
|
|
416
|
+
() => new Map(renderedDataColumns.map((col, index) => [col.field, index])),
|
|
417
|
+
[renderedDataColumns]
|
|
418
|
+
);
|
|
216
419
|
|
|
217
420
|
return {
|
|
218
421
|
effectiveColumns,
|
|
422
|
+
hierarchyField,
|
|
219
423
|
orderedColumns,
|
|
220
424
|
visibleOrderedColumns,
|
|
425
|
+
renderOrderedColumns: renderedDataColumns,
|
|
221
426
|
navigationColumns,
|
|
427
|
+
columnIndexMap,
|
|
222
428
|
columnWidths,
|
|
223
429
|
effectiveColumnOrder,
|
|
224
430
|
setInternalColumnOrder,
|
|
225
431
|
columnReorderHandlers,
|
|
432
|
+
moveColumn,
|
|
433
|
+
resetColumnOrder,
|
|
226
434
|
handleColumnResize,
|
|
227
435
|
};
|
|
228
436
|
}
|