@opencorestack/opengridx 2.1.0 → 3.0.1
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 +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- 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 -166
- 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 +6 -7
- package/docs/components/datagrid.md +15 -13
- 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 +90 -38
- 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 +126 -39
- 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 +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
- 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 +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- 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 +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- 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 +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- 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 +19 -4
- 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 +279 -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 +104 -5
- 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 +447 -243
- 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 +17 -5
- 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 +82 -51
- 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 +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -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 +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- 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 +278 -139
- 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 +55 -5
- 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 +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- 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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- 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 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { GridRowId, GridRowModel } from '../../types';
|
|
1
|
+
import { useState, useCallback, useRef, useEffect, useLayoutEffect, useMemo } from 'react';
|
|
2
|
+
import type { GridColDef, GridRowId, GridRowModel } from '../../types';
|
|
3
3
|
|
|
4
4
|
export interface GridEditingState {
|
|
5
5
|
editingCell: {
|
|
@@ -13,87 +13,247 @@ export interface GridEditingState {
|
|
|
13
13
|
export interface UseGridEditingParams<R extends GridRowModel> {
|
|
14
14
|
rows: R[];
|
|
15
15
|
getRowId: (row: R) => GridRowId;
|
|
16
|
+
/** Column definitions; used to find the edited column's `valueSetter`. */
|
|
17
|
+
columns?: GridColDef<R>[];
|
|
16
18
|
processRowUpdate?: (newRow: R, oldRow: R) => R | Promise<R>;
|
|
17
19
|
onProcessRowUpdateError?: (error: unknown) => void;
|
|
18
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Receives the committed row and the id of the row that was edited. Match on `id` (the key the
|
|
22
|
+
* grid resolved with getRowId), not on a field of the returned row.
|
|
23
|
+
*/
|
|
24
|
+
onRowChange?: (newRow: R, id: GridRowId) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface StopCellEditParams {
|
|
28
|
+
/** Discard the pending value instead of committing it. */
|
|
29
|
+
cancel?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Only act if this cell is the one being edited. Editors pass their own cell so that a late
|
|
32
|
+
* blur or unmount cannot end another cell's edit.
|
|
33
|
+
*/
|
|
34
|
+
id?: GridRowId;
|
|
35
|
+
field?: string;
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
type EditingCellState = GridEditingState['editingCell'];
|
|
39
|
+
type EditingCell = NonNullable<EditingCellState>;
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
interface PendingCommit {
|
|
42
|
+
value: unknown;
|
|
43
|
+
promise: Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const RESOLVED: Promise<void> = Promise.resolve();
|
|
47
|
+
|
|
48
|
+
/** A misuse of the editing contract (e.g. processRowUpdate returned nothing), as opposed to a consumer rejection. */
|
|
49
|
+
class OpenGridXEditError extends Error {}
|
|
50
|
+
|
|
51
|
+
function isSameValue(a: unknown, b: unknown): boolean {
|
|
52
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
53
|
+
return Object.is(a, b);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
57
|
+
return typeof value === 'object' && value !== null && typeof (value as { then?: unknown }).then === 'function';
|
|
58
|
+
}
|
|
31
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Cell edit sessions and their commits.
|
|
62
|
+
*
|
|
63
|
+
* - One commit per edit: a stop while a commit is in flight (Enter then blur, Tab then the blur that
|
|
64
|
+
* moving focus causes) shares that commit instead of calling `processRowUpdate` again. A stop that
|
|
65
|
+
* arrives after the value changed again commits the newer value once the first commit settles.
|
|
66
|
+
* - A commit that settles late only closes the editor if that same session is still open with the
|
|
67
|
+
* committed value; it never closes another cell's edit or drops text typed in the meantime. Its
|
|
68
|
+
* row is always applied: `processRowUpdate` has already run, and may already have persisted it.
|
|
69
|
+
* - Starting an edit on another cell commits the current one first, so an edit whose editor is gone
|
|
70
|
+
* (for example scrolled out of the render window) is never silently dropped.
|
|
71
|
+
* - The returned functions keep their identity for the lifetime of the grid.
|
|
72
|
+
*/
|
|
73
|
+
export function useGridEditing<R extends GridRowModel>(params: UseGridEditingParams<R>) {
|
|
32
74
|
const [editingCell, setEditingCellState] = useState<EditingCellState>(null);
|
|
33
75
|
const editingCellRef = useRef<EditingCellState>(null);
|
|
76
|
+
// Id of the current edit session; bumped whenever a cell enters edit mode.
|
|
77
|
+
const sessionRef = useRef(0);
|
|
78
|
+
const pendingRef = useRef(new Map<number, PendingCommit>());
|
|
79
|
+
const warnedFieldsRef = useRef(new Set<string>());
|
|
80
|
+
const mountedRef = useRef(false);
|
|
34
81
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
// Latest props, read when an edit is committed so the callbacks below can stay stable.
|
|
83
|
+
const latestRef = useRef(params);
|
|
84
|
+
useLayoutEffect(() => {
|
|
85
|
+
latestRef.current = params;
|
|
86
|
+
});
|
|
39
87
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
value: editParams.value,
|
|
45
|
-
originalValue: editParams.value
|
|
46
|
-
});
|
|
47
|
-
}, [setEditingCell]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
mountedRef.current = true;
|
|
90
|
+
return () => { mountedRef.current = false; };
|
|
91
|
+
}, []);
|
|
48
92
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
93
|
+
const setEditingCell = useCallback((next: EditingCellState) => {
|
|
94
|
+
editingCellRef.current = next;
|
|
95
|
+
setEditingCellState(next);
|
|
96
|
+
}, []);
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
const commit = useCallback((token: number, cell: EditingCell): Promise<void> => {
|
|
99
|
+
const inFlight = pendingRef.current.get(token);
|
|
100
|
+
if (inFlight) {
|
|
101
|
+
if (isSameValue(inFlight.value, cell.value)) return inFlight.promise;
|
|
102
|
+
// The value changed while a commit was in flight: commit the newer value once it settles.
|
|
103
|
+
return inFlight.promise.then(() => {
|
|
104
|
+
const now = editingCellRef.current;
|
|
105
|
+
if (sessionRef.current !== token || !now || isSameValue(now.value, inFlight.value)) return;
|
|
106
|
+
return commit(token, now);
|
|
107
|
+
});
|
|
56
108
|
}
|
|
57
109
|
|
|
58
|
-
const
|
|
110
|
+
const isCurrent = () => sessionRef.current === token && editingCellRef.current !== null;
|
|
59
111
|
|
|
60
|
-
if (value
|
|
61
|
-
setEditingCell(null);
|
|
62
|
-
return;
|
|
112
|
+
if (isSameValue(cell.value, cell.originalValue)) {
|
|
113
|
+
if (isCurrent()) setEditingCell(null);
|
|
114
|
+
return RESOLVED;
|
|
63
115
|
}
|
|
64
116
|
|
|
65
|
-
const
|
|
117
|
+
const { rows, getRowId, columns, processRowUpdate } = latestRef.current;
|
|
118
|
+
const existingRow = rows.find(r => getRowId(r) === cell.id);
|
|
66
119
|
if (!existingRow) {
|
|
67
|
-
setEditingCell(null);
|
|
68
|
-
return;
|
|
120
|
+
if (isCurrent()) setEditingCell(null);
|
|
121
|
+
return RESOLVED;
|
|
69
122
|
}
|
|
70
123
|
|
|
71
|
-
const
|
|
124
|
+
const report = (error: unknown) => {
|
|
125
|
+
const onError = latestRef.current.onProcessRowUpdateError;
|
|
126
|
+
if (onError) {
|
|
127
|
+
onError(error);
|
|
128
|
+
} else if (process.env.NODE_ENV !== 'production' && error instanceof OpenGridXEditError) {
|
|
129
|
+
console.error(error.message);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
72
132
|
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
const apply = (processed: unknown) => {
|
|
134
|
+
if (typeof processed !== 'object' || processed === null) {
|
|
135
|
+
report(new OpenGridXEditError(
|
|
136
|
+
`[OpenGridX] processRowUpdate must return the updated row object (or a Promise of it); ` +
|
|
137
|
+
`it returned ${processed === null ? 'null' : typeof processed}. The edit was not saved and the editor stays open.`
|
|
138
|
+
));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
latestRef.current.onRowChange?.(processed as R, cell.id);
|
|
142
|
+
const now = editingCellRef.current;
|
|
143
|
+
if (sessionRef.current !== token || !now) return;
|
|
144
|
+
if (isSameValue(now.value, cell.value)) {
|
|
145
|
+
setEditingCell(null);
|
|
77
146
|
} else {
|
|
78
|
-
|
|
147
|
+
// Newer text was typed while this commit was in flight: keep editing it.
|
|
148
|
+
setEditingCell({ ...now, originalValue: cell.value });
|
|
79
149
|
}
|
|
80
|
-
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Register the commit before calling out, so a re-entrant stop joins it.
|
|
153
|
+
let settle!: () => void;
|
|
154
|
+
let fail!: (error: unknown) => void;
|
|
155
|
+
const promise = new Promise<void>((resolve, reject) => { settle = resolve; fail = reject; });
|
|
156
|
+
const entry: PendingCommit = { value: cell.value, promise };
|
|
157
|
+
pendingRef.current.set(token, entry);
|
|
158
|
+
const finish = () => {
|
|
159
|
+
if (pendingRef.current.get(token) === entry) pendingRef.current.delete(token);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let result: unknown;
|
|
163
|
+
try {
|
|
164
|
+
const colDef = columns?.find(c => c.field === cell.field);
|
|
165
|
+
const newRow = buildEditedRow(existingRow, cell, colDef, warnedFieldsRef.current);
|
|
166
|
+
result = processRowUpdate ? processRowUpdate(newRow, existingRow) : newRow;
|
|
81
167
|
} catch (error) {
|
|
82
|
-
|
|
168
|
+
// Consumer code (valueSetter / processRowUpdate) threw: report it, keep the editor open.
|
|
169
|
+
finish();
|
|
170
|
+
report(error);
|
|
171
|
+
settle();
|
|
172
|
+
return promise;
|
|
83
173
|
}
|
|
84
174
|
|
|
85
|
-
|
|
175
|
+
const complete = (processed: unknown) => {
|
|
176
|
+
finish();
|
|
177
|
+
try {
|
|
178
|
+
apply(processed);
|
|
179
|
+
settle();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// A failure while the grid stores the row is a grid error, not a processRowUpdate one.
|
|
182
|
+
fail(error);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
86
185
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
186
|
+
if (isPromiseLike(result)) {
|
|
187
|
+
Promise.resolve(result).then(complete, (error: unknown) => {
|
|
188
|
+
finish();
|
|
189
|
+
report(error);
|
|
190
|
+
settle();
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
complete(result);
|
|
90
194
|
}
|
|
91
|
-
|
|
195
|
+
return promise;
|
|
196
|
+
}, [setEditingCell]);
|
|
92
197
|
|
|
93
|
-
|
|
198
|
+
const startCellEdit = useCallback((editParams: { id: GridRowId; field: string; value: unknown }) => {
|
|
199
|
+
const current = editingCellRef.current;
|
|
200
|
+
if (current) {
|
|
201
|
+
if (current.id === editParams.id && current.field === editParams.field) return;
|
|
202
|
+
// Commit the edit in progress before switching cells.
|
|
203
|
+
void commit(sessionRef.current, current);
|
|
204
|
+
}
|
|
205
|
+
sessionRef.current += 1;
|
|
206
|
+
setEditingCell({
|
|
207
|
+
id: editParams.id,
|
|
208
|
+
field: editParams.field,
|
|
209
|
+
value: editParams.value,
|
|
210
|
+
originalValue: editParams.value
|
|
211
|
+
});
|
|
212
|
+
}, [commit, setEditingCell]);
|
|
213
|
+
|
|
214
|
+
const stopCellEdit = useCallback((stopParams?: StopCellEditParams): Promise<void> => {
|
|
215
|
+
const cell = editingCellRef.current;
|
|
216
|
+
if (!cell || !mountedRef.current) return RESOLVED;
|
|
217
|
+
if (stopParams?.id !== undefined && stopParams.id !== cell.id) return RESOLVED;
|
|
218
|
+
if (stopParams?.field !== undefined && stopParams.field !== cell.field) return RESOLVED;
|
|
219
|
+
|
|
220
|
+
if (stopParams?.cancel) {
|
|
221
|
+
setEditingCell(null);
|
|
222
|
+
return RESOLVED;
|
|
223
|
+
}
|
|
224
|
+
return commit(sessionRef.current, cell);
|
|
225
|
+
}, [commit, setEditingCell]);
|
|
226
|
+
|
|
227
|
+
const setEditCellValue = useCallback((editParams: { id: GridRowId; field: string; value: unknown }) => {
|
|
228
|
+
const current = editingCellRef.current;
|
|
229
|
+
if (!current || current.id !== editParams.id || current.field !== editParams.field) return;
|
|
230
|
+
setEditingCell({ ...current, value: editParams.value });
|
|
231
|
+
}, [setEditingCell]);
|
|
232
|
+
|
|
233
|
+
return useMemo(() => ({
|
|
94
234
|
editingCell,
|
|
95
235
|
startCellEdit,
|
|
96
236
|
stopCellEdit,
|
|
97
237
|
setEditCellValue
|
|
98
|
-
};
|
|
238
|
+
}), [editingCell, startCellEdit, stopCellEdit, setEditCellValue]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildEditedRow<R extends GridRowModel>(
|
|
242
|
+
row: R,
|
|
243
|
+
cell: EditingCell,
|
|
244
|
+
colDef: GridColDef<R> | undefined,
|
|
245
|
+
warnedFields: Set<string>
|
|
246
|
+
): R {
|
|
247
|
+
if (colDef?.valueSetter) {
|
|
248
|
+
return colDef.valueSetter({ value: cell.value, row, field: cell.field });
|
|
249
|
+
}
|
|
250
|
+
if (colDef?.valueGetter && process.env.NODE_ENV !== 'production' && !warnedFields.has(cell.field)) {
|
|
251
|
+
warnedFields.add(cell.field);
|
|
252
|
+
console.warn(
|
|
253
|
+
`[OpenGridX] Column "${cell.field}" is editable and has a valueGetter but no valueSetter. ` +
|
|
254
|
+
`The edited value was written to row["${cell.field}"], which the valueGetter may ignore, so the cell ` +
|
|
255
|
+
'can keep showing the old value. Add a valueSetter that maps the value back onto the row.'
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return { ...row, [cell.field]: cell.value };
|
|
99
259
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import type { GridColDef, GridFilterModel, GridPivotModel, GridRowModel, GridSortItem } from '../../types';
|
|
3
|
+
import { filterRows } from '../../utils/filtering';
|
|
4
|
+
import { buildColumnLookup } from '../../utils/columnLookup';
|
|
5
|
+
import { computePivot, splitPivotFilterModel, EMPTY_PIVOT_RESULT } from '../../utils/pivot';
|
|
6
|
+
|
|
7
|
+
const EMPTY_FILTER_MODEL: GridFilterModel = { items: [] };
|
|
8
|
+
const EMPTY_SORT_MODEL: GridSortItem[] = [];
|
|
9
|
+
|
|
10
|
+
export interface UseGridPivotParams {
|
|
11
|
+
rows: GridRowModel[];
|
|
12
|
+
columns: GridColDef[];
|
|
13
|
+
pivotMode: boolean;
|
|
14
|
+
pivotModel: GridPivotModel;
|
|
15
|
+
filterModel: GridFilterModel;
|
|
16
|
+
sortModel: GridSortItem[];
|
|
17
|
+
/** Pivoting is client-side: with a dataSource the grid only holds one page, so pivot mode is not applied. */
|
|
18
|
+
hasDataSource: boolean;
|
|
19
|
+
/** treeData or a rowGroupingModel was requested; both are turned off while pivoting. */
|
|
20
|
+
hierarchyRequested: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseGridPivotResult {
|
|
24
|
+
/** Pivot mode is on, the model is usable and the grid is client-side. */
|
|
25
|
+
isActive: boolean;
|
|
26
|
+
/** Sorted pivot data rows followed by the Grand Total row. */
|
|
27
|
+
rows: GridRowModel[];
|
|
28
|
+
columns: GridColDef[];
|
|
29
|
+
/**
|
|
30
|
+
* Filter and sort models for the row pipeline. While pivoting they are empty: the pivot already
|
|
31
|
+
* applied them (filters to the source rows or the pivot rows, sorting with the Grand Total kept last).
|
|
32
|
+
*/
|
|
33
|
+
pipelineFilterModel: GridFilterModel;
|
|
34
|
+
pipelineSortModel: GridSortItem[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* DataGrid's pivot stage. Source-field filters and the quick filter run on the source rows before
|
|
39
|
+
* pivoting, so pivot values and the Grand Total reflect them; conditions on generated value columns run
|
|
40
|
+
* on the pivot rows. Sorting orders the pivot data rows and never moves the Grand Total.
|
|
41
|
+
*/
|
|
42
|
+
export function useGridPivot(params: UseGridPivotParams): UseGridPivotResult {
|
|
43
|
+
const { rows, columns, pivotMode, pivotModel, filterModel, sortModel, hasDataSource, hierarchyRequested } = params;
|
|
44
|
+
const enabled = pivotMode && !hasDataSource;
|
|
45
|
+
|
|
46
|
+
const sourceFields = useMemo(() => new Set(columns.map((c) => c.field)), [columns]);
|
|
47
|
+
// No source column is on screen while pivoting (the row-label columns show the row fields
|
|
48
|
+
// whatever the visibility model says), so the quick filter searches every filterable source column.
|
|
49
|
+
const sourceLookup = useMemo(() => buildColumnLookup(columns), [columns]);
|
|
50
|
+
const splitFilter = useMemo(
|
|
51
|
+
() => splitPivotFilterModel(filterModel, sourceFields),
|
|
52
|
+
[filterModel, sourceFields],
|
|
53
|
+
);
|
|
54
|
+
const sourceRows = useMemo(
|
|
55
|
+
() => (enabled ? filterRows(rows, splitFilter.source, sourceLookup) : rows),
|
|
56
|
+
[enabled, rows, splitFilter.source, sourceLookup],
|
|
57
|
+
);
|
|
58
|
+
const pivot = useMemo(
|
|
59
|
+
() => (enabled
|
|
60
|
+
? computePivot(sourceRows, columns, pivotModel, { outputFilterModel: splitFilter.output, sortModel })
|
|
61
|
+
: EMPTY_PIVOT_RESULT),
|
|
62
|
+
[enabled, sourceRows, columns, pivotModel, splitFilter.output, sortModel],
|
|
63
|
+
);
|
|
64
|
+
const isActive = enabled && pivot.isValid;
|
|
65
|
+
|
|
66
|
+
const warnedRef = useRef({ dataSource: false, hierarchy: false });
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
69
|
+
if (pivotMode && hasDataSource && !warnedRef.current.dataSource) {
|
|
70
|
+
warnedRef.current.dataSource = true;
|
|
71
|
+
console.warn(
|
|
72
|
+
'[OpenGridX] `pivotMode` is ignored when a `dataSource` is set: pivoting is client-side and the grid ' +
|
|
73
|
+
'only holds the rows of one server page. Pivot on the server, or pass the full dataset as `rows`.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (isActive && hierarchyRequested && !warnedRef.current.hierarchy) {
|
|
77
|
+
warnedRef.current.hierarchy = true;
|
|
78
|
+
console.warn(
|
|
79
|
+
'[OpenGridX] `treeData` and `rowGroupingModel` are turned off while `pivotMode` is active: the pivot ' +
|
|
80
|
+
'rows are already grouped by the pivot row fields.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}, [pivotMode, hasDataSource, isActive, hierarchyRequested]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
isActive,
|
|
87
|
+
rows: isActive ? pivot.pivotRows : rows,
|
|
88
|
+
columns: isActive ? pivot.pivotColumns : columns,
|
|
89
|
+
pipelineFilterModel: isActive ? EMPTY_FILTER_MODEL : filterModel,
|
|
90
|
+
pipelineSortModel: isActive ? EMPTY_SORT_MODEL : sortModel,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { GridColDef, GridRowId, GridRowModel } from '../../types';
|
|
3
|
+
import type { GridSpanningResult } from './useGridSpanning';
|
|
4
|
+
|
|
5
|
+
interface RowRenderContext {
|
|
6
|
+
firstRowIndex: number;
|
|
7
|
+
lastRowIndex: number;
|
|
8
|
+
firstColumnIndex: number;
|
|
9
|
+
lastColumnIndex: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseGridSpanRowWindowParams {
|
|
13
|
+
spanning: GridSpanningResult;
|
|
14
|
+
renderContext: RowRenderContext;
|
|
15
|
+
offsetTop: number;
|
|
16
|
+
/** Cumulative heights of the center rows (layout output). */
|
|
17
|
+
cumulativeHeights: number[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extends the vertical render window back to the origin row of any row span that reaches the first
|
|
22
|
+
* rendered row. Only the origin renders the merged content, so without this the visible part of a
|
|
23
|
+
* span whose origin has scrolled above the overscan window would be blank.
|
|
24
|
+
*/
|
|
25
|
+
export function useGridSpanRowWindow(params: UseGridSpanRowWindowParams): { renderContext: RowRenderContext; offsetTop: number } {
|
|
26
|
+
const { spanning, renderContext, offsetTop, cumulativeHeights } = params;
|
|
27
|
+
|
|
28
|
+
return useMemo(() => {
|
|
29
|
+
if (!spanning.hasRowSpan) return { renderContext, offsetTop };
|
|
30
|
+
let first = Math.max(0, renderContext.firstRowIndex);
|
|
31
|
+
for (let start = spanning.getCenterRowSpanStart(first); start < first; start = spanning.getCenterRowSpanStart(first)) {
|
|
32
|
+
first = start;
|
|
33
|
+
}
|
|
34
|
+
if (first >= renderContext.firstRowIndex) return { renderContext, offsetTop };
|
|
35
|
+
return {
|
|
36
|
+
renderContext: { ...renderContext, firstRowIndex: first },
|
|
37
|
+
offsetTop: first > 0 ? cumulativeHeights[first - 1] ?? 0 : 0,
|
|
38
|
+
};
|
|
39
|
+
}, [spanning, renderContext, offsetTop, cumulativeHeights]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ColumnLayout<R extends GridRowModel> {
|
|
43
|
+
leftPinnedCols: GridColDef<R>[];
|
|
44
|
+
unpinnedColsWithWidth: (GridColDef<R> & { width: number })[];
|
|
45
|
+
rightPinnedCols: GridColDef<R>[];
|
|
46
|
+
unpinnedAccWidths: number[];
|
|
47
|
+
unpinnedTotalWidth: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UseGridSpanColumnWindowParams<R extends GridRowModel> {
|
|
51
|
+
spanning: GridSpanningResult;
|
|
52
|
+
/** Unpinned column window chosen by the virtualization. */
|
|
53
|
+
firstColumnIndex: number;
|
|
54
|
+
lastColumnIndex: number;
|
|
55
|
+
virtualColumns: GridColDef<R>[];
|
|
56
|
+
layout: ColumnLayout<R>;
|
|
57
|
+
/** Rows that will be rendered, with their resolved id (rows do not have to carry it in `row.id`). */
|
|
58
|
+
rows: { row: R; id?: GridRowId }[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Widens the horizontal render window so every colSpan in the rendered rows is either fully inside
|
|
63
|
+
* it or fully outside it. A span origin left of the window would otherwise render nothing while its
|
|
64
|
+
* covered columns render nothing either, shifting the rest of the row left; a span that runs past
|
|
65
|
+
* the right edge would overflow the right spacer.
|
|
66
|
+
*/
|
|
67
|
+
export function useGridSpanColumnWindow<R extends GridRowModel>(params: UseGridSpanColumnWindowParams<R>): GridColDef<R>[] {
|
|
68
|
+
const { spanning, firstColumnIndex, lastColumnIndex, virtualColumns, layout, rows } = params;
|
|
69
|
+
|
|
70
|
+
return useMemo(() => {
|
|
71
|
+
if (!spanning.hasColSpan || layout.unpinnedColsWithWidth.length === 0) return virtualColumns;
|
|
72
|
+
|
|
73
|
+
let first = firstColumnIndex;
|
|
74
|
+
let last = lastColumnIndex;
|
|
75
|
+
let changed = true;
|
|
76
|
+
while (changed) {
|
|
77
|
+
changed = false;
|
|
78
|
+
for (const { row, id } of rows) {
|
|
79
|
+
const ranges = spanning.getUnpinnedColSpanRanges(id ?? row.id);
|
|
80
|
+
if (!ranges) continue;
|
|
81
|
+
for (let k = 0; k < ranges.length; k += 2) {
|
|
82
|
+
const start = ranges[k];
|
|
83
|
+
const end = ranges[k + 1];
|
|
84
|
+
if (end < first || start > last) continue;
|
|
85
|
+
if (start < first) { first = start; changed = true; }
|
|
86
|
+
if (end > last) { last = end; changed = true; }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (first === firstColumnIndex && last === lastColumnIndex) return virtualColumns;
|
|
92
|
+
|
|
93
|
+
const { leftPinnedCols, unpinnedColsWithWidth, rightPinnedCols, unpinnedAccWidths, unpinnedTotalWidth } = layout;
|
|
94
|
+
const leftSpacerWidth = first > 0 ? unpinnedAccWidths[first - 1] : 0;
|
|
95
|
+
const rightSpacerWidth = unpinnedTotalWidth - unpinnedAccWidths[last];
|
|
96
|
+
return [
|
|
97
|
+
...leftPinnedCols,
|
|
98
|
+
...(leftSpacerWidth > 0 ? [{ field: '__spacer_left__', width: leftSpacerWidth, isSpacer: true } as GridColDef<R>] : []),
|
|
99
|
+
...unpinnedColsWithWidth.slice(first, last + 1),
|
|
100
|
+
...(rightSpacerWidth > 0 ? [{ field: '__spacer_right__', width: rightSpacerWidth, isSpacer: true } as GridColDef<R>] : []),
|
|
101
|
+
...rightPinnedCols,
|
|
102
|
+
];
|
|
103
|
+
}, [spanning, firstColumnIndex, lastColumnIndex, virtualColumns, layout, rows]);
|
|
104
|
+
}
|