@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,30 +1,46 @@
|
|
|
1
1
|
import type { GridAggregationModel, GridAggregationResult, GridColDef, GridRowModel } from '../../types';
|
|
2
|
+
import { formatLocalDate, getCellValue, toNumber } from '../values';
|
|
2
3
|
|
|
3
4
|
export type BuiltInAggFn = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'unique';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A value that carries no data for aggregation: null, undefined, or a string that is empty or whitespace
|
|
8
|
+
* only. Blank cells come from edits, CSV imports and form inputs; they must not count as zero or as an item.
|
|
9
|
+
*/
|
|
10
|
+
/** Stable empty model: no aggregation (e.g. while pivoting, where the Grand Total row is the aggregate). */
|
|
11
|
+
export const NO_AGGREGATION_MODEL: GridAggregationModel = {};
|
|
12
|
+
|
|
13
|
+
export const isEmptyAggregateValue = (v: unknown): boolean =>
|
|
14
|
+
v == null || (typeof v === 'string' && v.trim() === '');
|
|
15
|
+
|
|
16
|
+
// Numeric functions read values with the grid's shared toNumber (as filtering does): numbers, numeric
|
|
17
|
+
// strings and Dates (as timestamps). Booleans, arrays and blank strings are skipped, since Number()
|
|
18
|
+
// would coerce them to 0 or 1 and silently corrupt avg / min / max.
|
|
5
19
|
const toNumbers = (values: unknown[]): number[] => {
|
|
6
20
|
const nums: number[] = [];
|
|
7
21
|
for (const v of values) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!isNaN(n)) nums.push(n);
|
|
22
|
+
const n = toNumber(v);
|
|
23
|
+
if (n !== null) nums.push(n);
|
|
11
24
|
}
|
|
12
25
|
return nums;
|
|
13
26
|
};
|
|
14
27
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// The smallest / largest value. A loop, not Math.min(...nums): spreading >~120k arguments throws
|
|
29
|
+
// RangeError. The extreme is returned as it was given when it is a Date, so min / max of a date
|
|
30
|
+
// column is a date (formatted as one) rather than epoch milliseconds.
|
|
31
|
+
const extremeOf = (values: unknown[], isBetter: (a: number, b: number) => boolean): number | Date | null => {
|
|
32
|
+
let best: number | null = null;
|
|
33
|
+
let bestValue: unknown = null;
|
|
34
|
+
for (const v of values) {
|
|
35
|
+
const n = toNumber(v);
|
|
36
|
+
if (n === null) continue;
|
|
37
|
+
if (best === null || isBetter(n, best)) {
|
|
38
|
+
best = n;
|
|
39
|
+
bestValue = v;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (best === null) return null;
|
|
43
|
+
return bestValue instanceof Date ? bestValue : best;
|
|
28
44
|
};
|
|
29
45
|
|
|
30
46
|
export const AGGREGATION_FUNCTIONS: Record<BuiltInAggFn, (values: unknown[]) => unknown> = {
|
|
@@ -33,12 +49,27 @@ export const AGGREGATION_FUNCTIONS: Record<BuiltInAggFn, (values: unknown[]) =>
|
|
|
33
49
|
const nums = toNumbers(values);
|
|
34
50
|
return nums.length ? nums.reduce((acc, n) => acc + n, 0) / nums.length : null;
|
|
35
51
|
},
|
|
36
|
-
count: (values) => values.filter((v) => v
|
|
37
|
-
min: (values) =>
|
|
38
|
-
max: (values) =>
|
|
39
|
-
unique: (values) => new Set(values.filter((v) => v
|
|
52
|
+
count: (values) => values.filter((v) => !isEmptyAggregateValue(v)).length,
|
|
53
|
+
min: (values) => extremeOf(values, (a, b) => a < b),
|
|
54
|
+
max: (values) => extremeOf(values, (a, b) => a > b),
|
|
55
|
+
unique: (values) => new Set(values.filter((v) => !isEmptyAggregateValue(v))).size,
|
|
40
56
|
};
|
|
41
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Whether `colDef` may be summarised with `fnName`: not when the column sets `aggregable: false`,
|
|
60
|
+
* and not with a function outside its `availableAggregationFunctions`. The footer, group rows,
|
|
61
|
+
* pivot values and exports all go through this, so a model entry the column forbids is ignored.
|
|
62
|
+
*/
|
|
63
|
+
export function isAggregationAllowed<R extends GridRowModel>(colDef: GridColDef<R> | undefined, fnName: string): boolean {
|
|
64
|
+
if (!colDef) return true;
|
|
65
|
+
if (colDef.aggregable === false) return false;
|
|
66
|
+
return !colDef.availableAggregationFunctions || colDef.availableAggregationFunctions.includes(fnName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Aggregates each `aggregationModel` field over `rows`. Cells are read through the column's
|
|
71
|
+
* `valueGetter` (getCellValue), so a computed column totals what its cells show.
|
|
72
|
+
*/
|
|
42
73
|
export function computeAggregations<R extends GridRowModel>(
|
|
43
74
|
rows: R[],
|
|
44
75
|
aggregationModel: GridAggregationModel,
|
|
@@ -48,15 +79,79 @@ export function computeAggregations<R extends GridRowModel>(
|
|
|
48
79
|
const result: GridAggregationResult = {};
|
|
49
80
|
for (const [field, fnName] of Object.entries(aggregationModel)) {
|
|
50
81
|
const colDef = columnsLookup.get(field);
|
|
51
|
-
if (colDef
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
82
|
+
if (!isAggregationAllowed(colDef, fnName)) continue;
|
|
54
83
|
const fn = AGGREGATION_FUNCTIONS[fnName as BuiltInAggFn];
|
|
55
84
|
if (!fn) {
|
|
56
85
|
console.warn(`[${warnPrefix}] Unknown aggregation function: "${fnName}"`);
|
|
57
86
|
continue;
|
|
58
87
|
}
|
|
59
|
-
result[field] = fn(rows.map((row) => row
|
|
88
|
+
result[field] = fn(rows.map((row) => getCellValue(row, field, colDef as GridColDef | undefined)));
|
|
60
89
|
}
|
|
61
90
|
return result;
|
|
62
91
|
}
|
|
92
|
+
|
|
93
|
+
/** Default display string for an aggregate: `—` when missing, locale-grouped numbers, avg to 2 decimals. */
|
|
94
|
+
export function formatAggregationValue(value: unknown, fnName: string): string {
|
|
95
|
+
if (value == null) return '—';
|
|
96
|
+
if (typeof value === 'number') {
|
|
97
|
+
if (fnName === 'avg') {
|
|
98
|
+
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
99
|
+
}
|
|
100
|
+
return value.toLocaleString();
|
|
101
|
+
}
|
|
102
|
+
return String(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const COUNT_FUNCTIONS = new Set<string>(['count', 'unique']);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether an aggregation result is a number of values rather than a value of the column's kind:
|
|
109
|
+
* a count of a currency column is not an amount, so the column's valueFormatter must not format it.
|
|
110
|
+
*/
|
|
111
|
+
export function isCountAggregation(fnName: string | undefined): boolean {
|
|
112
|
+
return fnName !== undefined && COUNT_FUNCTIONS.has(fnName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The aggregation result in the column's own terms: min / max of a `type: 'date'` column given as
|
|
117
|
+
* epoch milliseconds (as a server may return it) becomes a Date again.
|
|
118
|
+
*/
|
|
119
|
+
export function normalizeAggregateValue<R extends GridRowModel>(
|
|
120
|
+
colDef: GridColDef<R> | undefined,
|
|
121
|
+
fnName: string | undefined,
|
|
122
|
+
value: unknown,
|
|
123
|
+
): unknown {
|
|
124
|
+
if (colDef?.type === 'date' && (fnName === 'min' || fnName === 'max') && typeof value === 'number' && Number.isFinite(value)) {
|
|
125
|
+
return new Date(value);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Display text for an aggregate of `colDef`'s values, used by the footer, group rows, pivot cells and
|
|
132
|
+
* every exporter so they all show the same thing.
|
|
133
|
+
* - count / unique are plain counts in the default format; the column's valueFormatter is not applied.
|
|
134
|
+
* - sum / avg / min / max go through the column's valueFormatter. Its `row` is the record of aggregated
|
|
135
|
+
* values (the group row, the pivot row, or the footer's totals). A formatter that throws, typically
|
|
136
|
+
* because it reads row data an aggregate does not have, falls back to the default format.
|
|
137
|
+
* - Without a formatter, dates show as locale dates and numbers in the default format.
|
|
138
|
+
*/
|
|
139
|
+
export function formatAggregateForColumn<R extends GridRowModel>(
|
|
140
|
+
value: unknown,
|
|
141
|
+
fnName: string,
|
|
142
|
+
colDef?: GridColDef<R>,
|
|
143
|
+
aggregates?: Record<string, unknown>,
|
|
144
|
+
): string {
|
|
145
|
+
if (value == null) return '—';
|
|
146
|
+
if (isCountAggregation(fnName)) return formatAggregationValue(value, fnName);
|
|
147
|
+
const normalized = normalizeAggregateValue(colDef, fnName, value);
|
|
148
|
+
if (colDef?.valueFormatter) {
|
|
149
|
+
try {
|
|
150
|
+
return String(colDef.valueFormatter({ value: normalized, row: (aggregates ?? {}) as R, field: colDef.field }) ?? '');
|
|
151
|
+
} catch {
|
|
152
|
+
// Fall through to the default format rather than failing the whole grid or export.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (normalized instanceof Date) return Number.isNaN(normalized.getTime()) ? '' : formatLocalDate(normalized);
|
|
156
|
+
return formatAggregationValue(normalized, fnName);
|
|
157
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface GridAriaRowLayoutParams {
|
|
2
|
+
/** Header rows: the column header row plus any column-group rows. */
|
|
3
|
+
headerRowCount: number;
|
|
4
|
+
pinnedTopCount: number;
|
|
5
|
+
pinnedBottomCount: number;
|
|
6
|
+
/** Centre rows rendered for the current page. */
|
|
7
|
+
pageRowCount: number;
|
|
8
|
+
/** Centre rows across every page (the server's rowCount under server pagination). */
|
|
9
|
+
totalCenterRowCount: number;
|
|
10
|
+
/** Centre rows on the pages before the current one. */
|
|
11
|
+
pageOffset: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GridAriaRowLayout {
|
|
15
|
+
/** `aria-rowcount` of the grid. */
|
|
16
|
+
rowCount: number;
|
|
17
|
+
/** `aria-rowindex` of the first top-pinned row. */
|
|
18
|
+
topBase: number;
|
|
19
|
+
/** Added to a centre row's `rowIndex` (which counts top-pinned rows) to get its `aria-rowindex`. */
|
|
20
|
+
centerOffset: number;
|
|
21
|
+
/** `aria-rowindex` of the first bottom-pinned row. */
|
|
22
|
+
bottomBase: number;
|
|
23
|
+
/** `rowIndex` of the first bottom-pinned row (top-pinned + centre rows of this page). */
|
|
24
|
+
bottomRowIndexOffset: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Row positions for assistive technology. Header rows come first; data rows are numbered across
|
|
29
|
+
* the whole dataset, so page 2 does not restart at 1 and bottom-pinned rows follow every page.
|
|
30
|
+
*/
|
|
31
|
+
export function getAriaRowLayout(params: GridAriaRowLayoutParams): GridAriaRowLayout {
|
|
32
|
+
const { headerRowCount, pinnedTopCount, pinnedBottomCount, pageRowCount, totalCenterRowCount, pageOffset } = params;
|
|
33
|
+
return {
|
|
34
|
+
rowCount: headerRowCount + pinnedTopCount + totalCenterRowCount + pinnedBottomCount,
|
|
35
|
+
topBase: headerRowCount + 1,
|
|
36
|
+
centerOffset: headerRowCount + pageOffset + 1,
|
|
37
|
+
bottomBase: headerRowCount + pinnedTopCount + totalCenterRowCount + 1,
|
|
38
|
+
bottomRowIndexOffset: pinnedTopCount + pageRowCount,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { GridColumnGroup, GridColumnGroupingModel, GridPinnedPosition } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Number of group header rows a model renders (its maximum nesting depth; 0 when there is none). */
|
|
4
|
+
export function getColumnGroupDepth(model?: GridColumnGroupingModel): number {
|
|
5
|
+
if (!model) return 0;
|
|
6
|
+
let depth = 0;
|
|
7
|
+
const walk = (group: GridColumnGroup, level: number) => {
|
|
8
|
+
depth = Math.max(depth, level);
|
|
9
|
+
for (const child of group.children) {
|
|
10
|
+
if (typeof child !== 'string') walk(child, level + 1);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
model.forEach(group => walk(group, 1));
|
|
14
|
+
return depth;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Maps every leaf field to its group path, outermost group first. */
|
|
18
|
+
export function getColumnGroupPaths(model?: GridColumnGroupingModel): Map<string, GridColumnGroup[]> {
|
|
19
|
+
const paths = new Map<string, GridColumnGroup[]>();
|
|
20
|
+
if (!model) return paths;
|
|
21
|
+
const walk = (group: GridColumnGroup, path: GridColumnGroup[]) => {
|
|
22
|
+
const next = [...path, group];
|
|
23
|
+
for (const child of group.children) {
|
|
24
|
+
if (typeof child === 'string') {
|
|
25
|
+
if (!paths.has(child)) paths.set(child, next);
|
|
26
|
+
} else {
|
|
27
|
+
walk(child, next);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
model.forEach(group => walk(group, []));
|
|
32
|
+
return paths;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether moving `fromField` to the position of `toField` keeps every column group contiguous:
|
|
37
|
+
* both fields must sit in the same innermost group (or both be ungrouped).
|
|
38
|
+
*/
|
|
39
|
+
export function canReorderWithinColumnGroups(model: GridColumnGroupingModel | undefined, fromField: string, toField: string): boolean {
|
|
40
|
+
if (!model || model.length === 0) return true;
|
|
41
|
+
const paths = getColumnGroupPaths(model);
|
|
42
|
+
const from = paths.get(fromField);
|
|
43
|
+
const to = paths.get(toField);
|
|
44
|
+
return (from?.[from.length - 1] ?? null) === (to?.[to.length - 1] ?? null);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ColumnGroupRowColumn {
|
|
48
|
+
field: string;
|
|
49
|
+
width: number;
|
|
50
|
+
flex?: number;
|
|
51
|
+
pinned: GridPinnedPosition | null;
|
|
52
|
+
/** Horizontal-virtualization spacer: counts as ungrouped width. */
|
|
53
|
+
isSpacer?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ColumnGroupRowCell {
|
|
57
|
+
key: string;
|
|
58
|
+
isGroup: boolean;
|
|
59
|
+
label: string;
|
|
60
|
+
headerClassName?: string;
|
|
61
|
+
width: number;
|
|
62
|
+
flexGrow: number;
|
|
63
|
+
/** Data (non-spacer) fields the cell covers, in order. */
|
|
64
|
+
fields: string[];
|
|
65
|
+
/** Index of the first covered data column among all rendered data columns. */
|
|
66
|
+
firstDataIndex: number;
|
|
67
|
+
pinned: GridPinnedPosition | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds one group header row (level 0 = outermost). Consecutive columns that share the group at
|
|
72
|
+
* `level` and the same pinned section form one cell; consecutive ungrouped columns form one filler.
|
|
73
|
+
* A group whose columns are not adjacent (hidden columns excepted) renders one cell per run.
|
|
74
|
+
*/
|
|
75
|
+
export function buildColumnGroupRow(
|
|
76
|
+
columns: ColumnGroupRowColumn[],
|
|
77
|
+
paths: Map<string, GridColumnGroup[]>,
|
|
78
|
+
level: number,
|
|
79
|
+
): ColumnGroupRowCell[] {
|
|
80
|
+
const cells: ColumnGroupRowCell[] = [];
|
|
81
|
+
let dataIndex = 0;
|
|
82
|
+
let current: ColumnGroupRowCell | null = null;
|
|
83
|
+
let currentGroup: GridColumnGroup | null = null;
|
|
84
|
+
|
|
85
|
+
for (const column of columns) {
|
|
86
|
+
const group = column.isSpacer ? null : (paths.get(column.field)?.[level] ?? null);
|
|
87
|
+
if (current === null || currentGroup !== group || current.pinned !== column.pinned) {
|
|
88
|
+
current = {
|
|
89
|
+
key: `${group ? group.groupId : 'filler'}-${level}-${column.field}`,
|
|
90
|
+
isGroup: group !== null,
|
|
91
|
+
label: group?.headerName ?? '',
|
|
92
|
+
headerClassName: group?.headerClassName,
|
|
93
|
+
width: 0,
|
|
94
|
+
flexGrow: 0,
|
|
95
|
+
fields: [],
|
|
96
|
+
firstDataIndex: dataIndex,
|
|
97
|
+
pinned: column.pinned,
|
|
98
|
+
};
|
|
99
|
+
currentGroup = group;
|
|
100
|
+
cells.push(current);
|
|
101
|
+
}
|
|
102
|
+
current.width += column.width;
|
|
103
|
+
current.flexGrow += column.flex ?? 0;
|
|
104
|
+
if (!column.isSpacer) {
|
|
105
|
+
current.fields.push(column.field);
|
|
106
|
+
dataIndex++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return cells;
|
|
110
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GridColDef } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Column information the client-side filter and sort need: the column definition for each field
|
|
5
|
+
* (for `valueGetter` and `type`) and the columns the quick filter searches.
|
|
6
|
+
*/
|
|
7
|
+
export interface GridColumnLookup {
|
|
8
|
+
/** Column definitions keyed by field. */
|
|
9
|
+
byField: ReadonlyMap<string, GridColDef>;
|
|
10
|
+
/** Visible, filterable columns. The quick filter searches only these. */
|
|
11
|
+
quickFilterColumns: readonly GridColDef[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the lookup from the grid's columns. Columns hidden through `columnVisibilityModel`,
|
|
16
|
+
* columns with `filterable: false` and internal `__`-prefixed columns are not searched by the
|
|
17
|
+
* quick filter; column filter items still apply to every column.
|
|
18
|
+
*/
|
|
19
|
+
export function buildColumnLookup(
|
|
20
|
+
columns: readonly GridColDef[],
|
|
21
|
+
columnVisibilityModel: Record<string, boolean> = {},
|
|
22
|
+
): GridColumnLookup {
|
|
23
|
+
const byField = new Map<string, GridColDef>();
|
|
24
|
+
const quickFilterColumns: GridColDef[] = [];
|
|
25
|
+
for (const col of columns) {
|
|
26
|
+
byField.set(col.field, col);
|
|
27
|
+
if (col.isSpacer || col.field.startsWith('__')) continue;
|
|
28
|
+
if (col.filterable === false) continue;
|
|
29
|
+
if (columnVisibilityModel[col.field] === false) continue;
|
|
30
|
+
quickFilterColumns.push(col);
|
|
31
|
+
}
|
|
32
|
+
return { byField, quickFilterColumns };
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GridColDef, GridRowModel } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Clamps a column width the way CSS applies `min-width` / `max-width` (min-width wins a conflict). */
|
|
4
|
+
export function clampColumnWidth(width: number, minWidth?: number, maxWidth?: number): number {
|
|
5
|
+
return Math.max(minWidth ?? 0, Math.min(maxWidth ?? Infinity, width));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The width a rendered column occupies: the user's resize override, else its width (the
|
|
10
|
+
* layout-resolved pixel width for grid columns), clamped to `minWidth` / `maxWidth` exactly as
|
|
11
|
+
* the cell and header CSS clamp it. Non-numeric widths fall back to `fallback`.
|
|
12
|
+
*/
|
|
13
|
+
export function getRenderedColumnWidth<R extends GridRowModel>(
|
|
14
|
+
col: GridColDef<R>,
|
|
15
|
+
columnWidths: Record<string, number>,
|
|
16
|
+
fallback = 100,
|
|
17
|
+
): number {
|
|
18
|
+
if (col.isSpacer) return typeof col.width === 'number' ? col.width : 0;
|
|
19
|
+
const raw = columnWidths[col.field] ?? col.width;
|
|
20
|
+
const width = typeof raw === 'number' && Number.isFinite(raw) ? raw : fallback;
|
|
21
|
+
return clampColumnWidth(width, col.minWidth, col.maxWidth);
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ServerDrivenParams {
|
|
2
|
+
dataSource?: unknown;
|
|
3
|
+
paginationMode?: string;
|
|
4
|
+
sortingMode?: string;
|
|
5
|
+
filterMode?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* True when a dataSource is present and the server shapes the rows: any of pagination (including
|
|
10
|
+
* infinite scroll), sorting or filtering is done on the server. The grid then holds only the rows
|
|
11
|
+
* the server returned, so anything computed over "all rows" (such as aggregation totals) must come
|
|
12
|
+
* from the server. With every mode on 'client' the grid fetches all rows once and computes
|
|
13
|
+
* everything itself.
|
|
14
|
+
*/
|
|
15
|
+
export function isServerDrivenDataSource({ dataSource, paginationMode, sortingMode, filterMode }: ServerDrivenParams): boolean {
|
|
16
|
+
return Boolean(dataSource) && (
|
|
17
|
+
paginationMode === 'server'
|
|
18
|
+
|| paginationMode === 'infinite'
|
|
19
|
+
|| sortingMode === 'server'
|
|
20
|
+
|| filterMode === 'server'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `endRow` of a request for every row: sent when the grid pages on the client (`paginationMode`
|
|
26
|
+
* 'client') and when it loads the children of a server-side tree node. `endRow` is exclusive, and
|
|
27
|
+
* `Number.MAX_SAFE_INTEGER` keeps both `rows.slice(startRow, endRow)` and
|
|
28
|
+
* `LIMIT endRow - startRow` returning everything while staying JSON-serializable.
|
|
29
|
+
*/
|
|
30
|
+
export const GRID_ALL_ROWS_END_ROW = Number.MAX_SAFE_INTEGER;
|
|
31
|
+
|
|
32
|
+
/** Shown when a failed getRows call gives no usable message. */
|
|
33
|
+
export const DATA_SOURCE_ERROR_FALLBACK_MESSAGE = 'An unexpected error occurred while loading the data.';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The message of the value a getRows call rejected with: an `Error`, a string, or any object with a
|
|
37
|
+
* string `message` (the usual shape of fetch and HTTP-client errors).
|
|
38
|
+
*/
|
|
39
|
+
export function getDataSourceErrorMessage(error: unknown): string {
|
|
40
|
+
if (typeof error === 'string' && error) return error;
|
|
41
|
+
if (typeof error === 'object' && error !== null && 'message' in error
|
|
42
|
+
&& typeof error.message === 'string' && error.message) {
|
|
43
|
+
return error.message;
|
|
44
|
+
}
|
|
45
|
+
return DATA_SOURCE_ERROR_FALLBACK_MESSAGE;
|
|
46
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GridDetailPanelHeight, GridRowId, GridRowMeta } from '../types';
|
|
2
|
+
import { callSafely } from './values';
|
|
3
|
+
|
|
4
|
+
/** Panel height used when `getDetailPanelHeight` is omitted or returns something unusable. */
|
|
5
|
+
export const DEFAULT_DETAIL_PANEL_HEIGHT = 200;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalises a `getDetailPanelHeight` result. The layout and the rendered panel both go through
|
|
9
|
+
* this, so they always agree: `0` is a 0px panel, `'auto'` fits the content, and a missing,
|
|
10
|
+
* negative or non-numeric value is the 200px default.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveDetailPanelHeight(height: GridDetailPanelHeight | string | null | undefined): number | 'auto' {
|
|
13
|
+
if (height === 'auto') return 'auto';
|
|
14
|
+
const value = typeof height === 'string' ? Number.parseFloat(height) : height;
|
|
15
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return value;
|
|
16
|
+
return DEFAULT_DETAIL_PANEL_HEIGHT;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calls `getDetailPanelHeight`; a callback that throws gets the default height (with a one-time
|
|
21
|
+
* dev warning) instead of crashing the grid. The layout and both row renderers call this.
|
|
22
|
+
*/
|
|
23
|
+
export function callGetDetailPanelHeight<P>(
|
|
24
|
+
getDetailPanelHeight: ((params: P) => GridDetailPanelHeight) | undefined,
|
|
25
|
+
params: P,
|
|
26
|
+
): GridDetailPanelHeight | undefined {
|
|
27
|
+
if (!getDetailPanelHeight) return undefined;
|
|
28
|
+
return callSafely<GridDetailPanelHeight | undefined>(
|
|
29
|
+
() => getDetailPanelHeight(params),
|
|
30
|
+
undefined,
|
|
31
|
+
'getDetailPanelHeight',
|
|
32
|
+
'getDetailPanelHeight threw; the panel uses the default height',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The height the layout reserves for an expanded panel: its fixed height, or for an `'auto'`
|
|
38
|
+
* panel the height it was measured at (the default until it has rendered once).
|
|
39
|
+
*/
|
|
40
|
+
export function getDetailPanelLayoutHeight(
|
|
41
|
+
height: GridDetailPanelHeight | string | null | undefined,
|
|
42
|
+
measuredHeight: number | undefined,
|
|
43
|
+
): number {
|
|
44
|
+
const resolved = resolveDetailPanelHeight(height);
|
|
45
|
+
if (resolved === 'auto') return measuredHeight ?? DEFAULT_DETAIL_PANEL_HEIGHT;
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The expanded detail-panel ids that can actually show a panel: synthetic hierarchy rows
|
|
51
|
+
* (row-grouping groups, generated tree parents) have no detail panel, so an id of one in a
|
|
52
|
+
* controlled `detailPanelExpandedRowIds` is ignored. Returns the same set when nothing is dropped.
|
|
53
|
+
*/
|
|
54
|
+
export function getDetailPanelRowIds(
|
|
55
|
+
expandedRowIds: Set<GridRowId>,
|
|
56
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>,
|
|
57
|
+
): Set<GridRowId> {
|
|
58
|
+
if (rowMetaMap.size === 0 || expandedRowIds.size === 0) return expandedRowIds;
|
|
59
|
+
let dropped = false;
|
|
60
|
+
const ids = new Set<GridRowId>();
|
|
61
|
+
for (const id of expandedRowIds) {
|
|
62
|
+
if (rowMetaMap.get(id)?.isGroupRow) dropped = true;
|
|
63
|
+
else ids.add(id);
|
|
64
|
+
}
|
|
65
|
+
return dropped ? ids : expandedRowIds;
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML5 drag-and-drop helpers shared by row and column reorder.
|
|
3
|
+
*
|
|
4
|
+
* Each reorder drag puts a MIME type of its own into the DataTransfer. Firefox only starts a drag
|
|
5
|
+
* when dragstart sets some data, and drop targets use the type to accept only the grid's own drag,
|
|
6
|
+
* so a file, a text selection or another grid's drag is never taken for a reorder.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const ROW_DRAG_MIME = 'application/x-ogx-row';
|
|
10
|
+
export const COLUMN_DRAG_MIME = 'application/x-ogx-column';
|
|
11
|
+
|
|
12
|
+
interface DragEventLike {
|
|
13
|
+
dataTransfer?: Partial<Pick<DataTransfer, 'setData' | 'types'>> | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Stores the drag token (and a plain-text copy) on dragstart. */
|
|
17
|
+
export function setDragToken(event: DragEventLike, mime: string, value: string): void {
|
|
18
|
+
const dataTransfer = event.dataTransfer;
|
|
19
|
+
if (!dataTransfer || typeof dataTransfer.setData !== 'function') return;
|
|
20
|
+
dataTransfer.setData(mime, value);
|
|
21
|
+
dataTransfer.setData('text/plain', value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* False when the drag carries data types and the grid's token is not among them. When the
|
|
26
|
+
* browser (or a test double) exposes no type list, the drag is not rejected on that ground.
|
|
27
|
+
*/
|
|
28
|
+
export function hasDragToken(event: DragEventLike, mime: string): boolean {
|
|
29
|
+
const types = event.dataTransfer?.types;
|
|
30
|
+
if (!types) return true;
|
|
31
|
+
return Array.from(types).includes(mime);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Calls `onEnd` once when the drag that started on `source` ends. The listener sits on the
|
|
36
|
+
* source node itself, so it also fires when that node has been unmounted mid-drag (a virtualized
|
|
37
|
+
* row scrolled out of the render window), which React's delegated `onDragEnd` never sees.
|
|
38
|
+
*/
|
|
39
|
+
export function onNativeDragEnd(source: EventTarget | null | undefined, onEnd: () => void): () => void {
|
|
40
|
+
if (!source || typeof source.addEventListener !== 'function') return () => {};
|
|
41
|
+
const handler = () => onEnd();
|
|
42
|
+
source.addEventListener('dragend', handler, { once: true });
|
|
43
|
+
return () => source.removeEventListener('dragend', handler);
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GridCellParams, GridRowMeta, GridRowModel } from '../../types';
|
|
2
|
+
|
|
3
|
+
// Cells are read through the one `getCellValue` in `lib/utils/values`.
|
|
4
|
+
|
|
5
|
+
export interface CellEditabilityParams<R extends GridRowModel = GridRowModel> extends GridCellParams<R> {
|
|
6
|
+
rowMeta?: GridRowMeta;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether a body cell may enter edit mode. Double-click, Enter, Tab stops and `aria-readonly` all use
|
|
11
|
+
* this one rule:
|
|
12
|
+
* - the column must have `editable: true` (`isCellEditable` can only restrict, never grant);
|
|
13
|
+
* - the row must be a real data row, not a synthetic row-grouping header or an auto-generated tree
|
|
14
|
+
* ancestor (`rowMeta.isGroupRow`). Tree-data parents are real rows and stay editable;
|
|
15
|
+
* - `isCellEditable`, when given, must return true. A predicate that throws counts as read-only.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveCellEditable<R extends GridRowModel>(
|
|
18
|
+
params: CellEditabilityParams<R>,
|
|
19
|
+
isCellEditable?: (params: GridCellParams<R>) => boolean
|
|
20
|
+
): boolean {
|
|
21
|
+
const { row, field, value, colDef, rowIndex, colIndex, rowMeta } = params;
|
|
22
|
+
if (!colDef.editable) return false;
|
|
23
|
+
if (rowMeta?.isGroupRow || row._isSkeleton) return false;
|
|
24
|
+
if (!isCellEditable) return true;
|
|
25
|
+
try {
|
|
26
|
+
return Boolean(isCellEditable({ row, field, value, colDef, rowIndex, colIndex }));
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|