@opencorestack/opengridx 0.1.6 → 0.1.8
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 +30 -1
- package/README.md +9 -0
- package/dist/index.d.ts +12 -1
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +2505 -2450
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +177 -0
- package/docs/README.md +60 -0
- package/docs/components/cell.md +44 -0
- package/docs/components/column-group-header.md +57 -0
- package/docs/components/column-resize.md +63 -0
- package/docs/components/column-visibility.md +61 -0
- package/docs/components/datagrid.md +70 -0
- package/docs/components/filter-panel.md +26 -0
- package/docs/components/header.md +39 -0
- package/docs/components/pagination.md +34 -0
- package/docs/components/row.md +31 -0
- package/docs/components/toolbar.md +40 -0
- package/docs/components/tooltip.md +68 -0
- package/docs/customization/slots-api.md +227 -0
- package/docs/customization/theming.md +293 -0
- package/docs/features/aggregation-pivot.md +79 -0
- package/docs/features/cell-spanning.md +520 -0
- package/docs/features/clipboard.md +76 -0
- package/docs/features/custom-pagination.md +143 -0
- package/docs/features/data-source.md +72 -0
- package/docs/features/editing-reordering.md +99 -0
- package/docs/features/export-guide.md +434 -0
- package/docs/features/filtering.md +83 -0
- package/docs/features/infinite-scroll.md +87 -0
- package/docs/features/list-view.md +65 -0
- package/docs/features/loading-states.md +68 -0
- package/docs/features/master-detail.md +46 -0
- package/docs/features/pinning.md +67 -0
- package/docs/features/selection.md +70 -0
- package/docs/features/sorting-pagination.md +66 -0
- package/docs/features/state-persistence.md +67 -0
- package/docs/features/tree-data-grouping.md +67 -0
- package/docs/features/virtualization.md +188 -0
- package/docs/roadmap.md +71 -0
- package/lib/components/Cell/Cell.css +119 -0
- package/lib/components/Cell/Cell.tsx +264 -0
- package/lib/components/Cell/GridEditInputCell.css +62 -0
- package/lib/components/Cell/GridEditInputCell.tsx +152 -0
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +67 -0
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +220 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +31 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +85 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +137 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +190 -0
- package/lib/components/DataGrid/DataGrid.css +224 -0
- package/lib/components/DataGrid/DataGrid.tsx +2269 -0
- package/lib/components/DetailPanel/DetailPanel.css +36 -0
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -0
- package/lib/components/FilterPanel/FilterPanel.css +196 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +214 -0
- package/lib/components/FilterPanel/index.ts +1 -0
- package/lib/components/Header/ColumnMenu.css +68 -0
- package/lib/components/Header/ColumnMenu.tsx +249 -0
- package/lib/components/Header/Header.css +224 -0
- package/lib/components/Header/Header.tsx +516 -0
- package/lib/components/ListView/ListView.css +112 -0
- package/lib/components/ListView/ListViewRow.tsx +63 -0
- package/lib/components/Pagination/Pagination.css +91 -0
- package/lib/components/Pagination/Pagination.tsx +151 -0
- package/lib/components/QuickFilter/QuickFilter.css +23 -0
- package/lib/components/QuickFilter/QuickFilter.tsx +79 -0
- package/lib/components/Row/Row.css +126 -0
- package/lib/components/Row/Row.tsx +377 -0
- package/lib/components/Skeleton/Skeleton.css +55 -0
- package/lib/components/Skeleton/Skeleton.tsx +22 -0
- package/lib/components/SkeletonRow/SkeletonRow.css +32 -0
- package/lib/components/SkeletonRow/SkeletonRow.tsx +89 -0
- package/lib/components/SkeletonRow/index.ts +1 -0
- package/lib/components/Toolbar/GlobalSearch.css +110 -0
- package/lib/components/Toolbar/GlobalSearch.tsx +167 -0
- package/lib/components/Toolbar/GridToolbar.css +221 -0
- package/lib/components/Toolbar/GridToolbar.tsx +658 -0
- package/lib/components/Toolbar/PivotPanel.css +212 -0
- package/lib/components/Toolbar/PivotPanel.tsx +239 -0
- package/lib/components/Tooltip/Tooltip.css +59 -0
- package/lib/components/Tooltip/Tooltip.tsx +106 -0
- package/lib/components/ui/Button.css +89 -0
- package/lib/components/ui/Button.tsx +39 -0
- package/lib/components/ui/Checkbox.css +71 -0
- package/lib/components/ui/Checkbox.tsx +66 -0
- package/lib/components/ui/DragHandleIcon.css +19 -0
- package/lib/components/ui/DragHandleIcon.tsx +18 -0
- package/lib/components/ui/ExpandIcon.css +38 -0
- package/lib/components/ui/ExpandIcon.tsx +75 -0
- package/lib/components/ui/Input.css +59 -0
- package/lib/components/ui/Input.tsx +45 -0
- package/lib/components/ui/index.ts +15 -0
- package/lib/hooks/core/useDataGrid.ts +392 -0
- package/lib/hooks/core/useGridApiRef.ts +10 -0
- package/lib/hooks/features/useAggregation.ts +143 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -0
- package/lib/hooks/features/useGridDataSource.ts +190 -0
- package/lib/hooks/features/useGridEditing.ts +100 -0
- package/lib/hooks/features/useGridSpanning.ts +349 -0
- package/lib/hooks/features/usePivot.ts +174 -0
- package/lib/hooks/useColumnReorder.ts +114 -0
- package/lib/hooks/useRowGrouping.ts +302 -0
- package/lib/hooks/useRowReorder.ts +83 -0
- package/lib/hooks/useTreeData.ts +323 -0
- package/lib/index.ts +106 -0
- package/lib/state/index.ts +17 -0
- package/lib/state/types.ts +48 -0
- package/lib/state/useGridStateStorage.ts +113 -0
- package/lib/styles/base/reset.css +21 -0
- package/lib/styles/base/variables.css +161 -0
- package/lib/styles/opengridx.css +27 -0
- package/lib/styles/opengridx.css.d.ts +4 -0
- package/lib/theme/DataGridThemeProvider.tsx +276 -0
- package/lib/theme/defaultStyles.ts +0 -0
- package/lib/theme/index.ts +21 -0
- package/lib/theme/presets.ts +148 -0
- package/lib/theme/types.ts +155 -0
- package/lib/types/index.ts +764 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +516 -0
- package/lib/utils/export/index.ts +550 -0
- package/lib/utils/filtering/index.ts +240 -0
- package/lib/utils/pinning/index.ts +282 -0
- package/lib/utils/sorting/index.ts +55 -0
- package/lib/utils/virtualization/index.ts +106 -0
- package/lib/vite-env.d.ts +1 -0
- package/llms.txt +2 -1
- package/package.json +12 -4
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import type { GridColDef, GridRowModel } from '../../types';
|
|
4
|
+
import type { GridPivotModel, GridPivotAggFn } from '../../types';
|
|
5
|
+
|
|
6
|
+
function aggregate(values: number[], fn: GridPivotAggFn): number | null {
|
|
7
|
+
const nums = values.filter((v) => v != null && !isNaN(v));
|
|
8
|
+
if (nums.length === 0) return null;
|
|
9
|
+
switch (fn) {
|
|
10
|
+
case 'sum': return nums.reduce((a, b) => a + b, 0);
|
|
11
|
+
case 'avg': return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
12
|
+
case 'count': return nums.length;
|
|
13
|
+
case 'min': return Math.min(...nums);
|
|
14
|
+
case 'max': return Math.max(...nums);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeKey(row: GridRowModel, fields: string[]): string {
|
|
19
|
+
return fields.map((f) => String(row[f] ?? '')).join('\u0000');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UsePivotReturn {
|
|
23
|
+
pivotRows: GridRowModel[];
|
|
24
|
+
pivotColumns: GridColDef[];
|
|
25
|
+
colKeys: string[];
|
|
26
|
+
isValid: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function usePivot(
|
|
30
|
+
rawRows: GridRowModel[],
|
|
31
|
+
rawCols: GridColDef[],
|
|
32
|
+
model: GridPivotModel,
|
|
33
|
+
enabled: boolean,
|
|
34
|
+
): UsePivotReturn {
|
|
35
|
+
return useMemo<UsePivotReturn>(() => {
|
|
36
|
+
const empty: UsePivotReturn = { pivotRows: [], pivotColumns: [], colKeys: [], isValid: false };
|
|
37
|
+
|
|
38
|
+
if (!enabled) return empty;
|
|
39
|
+
|
|
40
|
+
const { rowFields, columnFields, valueFields } = model;
|
|
41
|
+
if (rowFields.length === 0 || valueFields.length === 0) return empty;
|
|
42
|
+
|
|
43
|
+
const colDefMap = new Map(rawCols.map((c) => [c.field, c]));
|
|
44
|
+
|
|
45
|
+
const colKeySet = new Set<string>();
|
|
46
|
+
for (const row of rawRows) {
|
|
47
|
+
colKeySet.add(makeKey(row, columnFields));
|
|
48
|
+
}
|
|
49
|
+
const colKeys = Array.from(colKeySet).sort();
|
|
50
|
+
|
|
51
|
+
const outerMap = new Map<string, {
|
|
52
|
+
labelValues: Record<string, any>;
|
|
53
|
+
bucket: Map<string, Map<string, number[]>>;
|
|
54
|
+
}>();
|
|
55
|
+
|
|
56
|
+
for (const row of rawRows) {
|
|
57
|
+
const rk = makeKey(row, rowFields);
|
|
58
|
+
if (!outerMap.has(rk)) {
|
|
59
|
+
const labelValues: Record<string, any> = {};
|
|
60
|
+
for (const f of rowFields) labelValues[f] = row[f];
|
|
61
|
+
outerMap.set(rk, { labelValues, bucket: new Map() });
|
|
62
|
+
}
|
|
63
|
+
const entry = outerMap.get(rk)!;
|
|
64
|
+
|
|
65
|
+
const ck = makeKey(row, columnFields);
|
|
66
|
+
if (!entry.bucket.has(ck)) entry.bucket.set(ck, new Map());
|
|
67
|
+
const colBucket = entry.bucket.get(ck)!;
|
|
68
|
+
|
|
69
|
+
for (const vf of valueFields) {
|
|
70
|
+
const v = Number(row[vf.field]);
|
|
71
|
+
if (!colBucket.has(vf.field)) colBucket.set(vf.field, []);
|
|
72
|
+
if (!isNaN(v)) colBucket.get(vf.field)!.push(v);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let rowIndex = 0;
|
|
77
|
+
const pivotRows: GridRowModel[] = [];
|
|
78
|
+
|
|
79
|
+
for (const [, entry] of outerMap) {
|
|
80
|
+
const pivotRow: GridRowModel = { id: rowIndex++, ...entry.labelValues };
|
|
81
|
+
|
|
82
|
+
for (const ck of colKeys) {
|
|
83
|
+
const colBucket = entry.bucket.get(ck);
|
|
84
|
+
for (const vf of valueFields) {
|
|
85
|
+
const cellKey = columnFields.length > 0
|
|
86
|
+
? `${ck}\u001f${vf.field}\u001f${vf.aggFn}`
|
|
87
|
+
: `${vf.field}\u001f${vf.aggFn}`;
|
|
88
|
+
const values = colBucket?.get(vf.field) ?? [];
|
|
89
|
+
pivotRow[cellKey] = values.length > 0 ? aggregate(values, vf.aggFn) : null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pivotRows.push(pivotRow);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Grand Total row ───────────────────────────────────────────────────
|
|
97
|
+
// Sum every numeric pivot cell across all pivot rows so users get
|
|
98
|
+
// column totals. The row uses the same synthetic field keys as regular
|
|
99
|
+
// pivot rows, so it renders with the correct valueFormatters.
|
|
100
|
+
// Label columns (rowFields) show 'Grand Total' in the first field, empty elsewhere.
|
|
101
|
+
const totalRow: GridRowModel = { id: '__pivot_grand_total__' };
|
|
102
|
+
for (let i = 0; i < rowFields.length; i++) {
|
|
103
|
+
totalRow[rowFields[i]] = i === 0 ? 'Grand Total' : '';
|
|
104
|
+
}
|
|
105
|
+
for (const ck of colKeys) {
|
|
106
|
+
for (const vf of valueFields) {
|
|
107
|
+
const cellKey = columnFields.length > 0
|
|
108
|
+
? `${ck}\u001f${vf.field}\u001f${vf.aggFn}`
|
|
109
|
+
: `${vf.field}\u001f${vf.aggFn}`;
|
|
110
|
+
// Sum the already-aggregated cell values across all pivot rows
|
|
111
|
+
const colTotal = pivotRows.reduce((acc, r) => {
|
|
112
|
+
const v = r[cellKey];
|
|
113
|
+
return acc + (v != null && !isNaN(Number(v)) ? Number(v) : 0);
|
|
114
|
+
}, 0);
|
|
115
|
+
totalRow[cellKey] = colTotal;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
pivotRows.push(totalRow);
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
const pivotColumns: GridColDef[] = [];
|
|
122
|
+
|
|
123
|
+
for (const f of rowFields) {
|
|
124
|
+
const orig = colDefMap.get(f);
|
|
125
|
+
pivotColumns.push({
|
|
126
|
+
field: f,
|
|
127
|
+
headerName: orig?.headerName ?? f,
|
|
128
|
+
width: orig?.width as number ?? 140,
|
|
129
|
+
sortable: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const ck of colKeys) {
|
|
134
|
+
|
|
135
|
+
const colLabel = columnFields.length > 0
|
|
136
|
+
? ck.split('\u0000').map((v, i) => {
|
|
137
|
+
const def = colDefMap.get(columnFields[i]);
|
|
138
|
+
return def?.headerName ? `${def.headerName}: ${v}` : v;
|
|
139
|
+
}).join(' / ')
|
|
140
|
+
: '';
|
|
141
|
+
|
|
142
|
+
for (const vf of valueFields) {
|
|
143
|
+
const orig = colDefMap.get(vf.field);
|
|
144
|
+
const cellKey = columnFields.length > 0
|
|
145
|
+
? `${ck}\u001f${vf.field}\u001f${vf.aggFn}`
|
|
146
|
+
: `${vf.field}\u001f${vf.aggFn}`;
|
|
147
|
+
const labelBase = vf.headerName ?? orig?.headerName ?? vf.field;
|
|
148
|
+
const headerName = colLabel
|
|
149
|
+
? `${colLabel} — ${labelBase} (${vf.aggFn})`
|
|
150
|
+
: `${labelBase} (${vf.aggFn})`;
|
|
151
|
+
|
|
152
|
+
pivotColumns.push({
|
|
153
|
+
field: cellKey,
|
|
154
|
+
headerName,
|
|
155
|
+
width: 140,
|
|
156
|
+
type: 'number',
|
|
157
|
+
align: 'right',
|
|
158
|
+
headerAlign: 'right',
|
|
159
|
+
sortable: true,
|
|
160
|
+
valueFormatter: ({ value }) => {
|
|
161
|
+
if (value == null) return '—';
|
|
162
|
+
const n = Number(value);
|
|
163
|
+
if (vf.aggFn === 'avg') return n.toFixed(2);
|
|
164
|
+
if (vf.aggFn === 'count') return n.toLocaleString();
|
|
165
|
+
if (orig?.valueFormatter) return orig.valueFormatter({ value, row: {} as any, field: vf.field });
|
|
166
|
+
return n.toLocaleString();
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { pivotRows, pivotColumns, colKeys, isValid: true };
|
|
173
|
+
}, [rawRows, rawCols, model, enabled]);
|
|
174
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
|
|
2
|
+
import { useState, useCallback, useRef } from 'react';
|
|
3
|
+
import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface UseColumnReorderProps<R extends GridRowModel = GridRowModel> {
|
|
6
|
+
columns: GridColDef<R>[];
|
|
7
|
+
onColumnOrderChange?: (params: GridColumnOrderChangeParams) => void;
|
|
8
|
+
disableColumnReorder?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseColumnReorderReturn {
|
|
12
|
+
draggedColumn: string | null;
|
|
13
|
+
dragOverColumn: string | null;
|
|
14
|
+
onDragStart: ((field: string) => (event: React.DragEvent) => void) | undefined;
|
|
15
|
+
onDragOver: ((field: string) => (event: React.DragEvent) => void) | undefined;
|
|
16
|
+
onDragEnd: (() => void) | undefined;
|
|
17
|
+
onDrop: ((targetField: string) => (event: React.DragEvent) => void) | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useColumnReorder<R extends GridRowModel = GridRowModel>(
|
|
21
|
+
props: UseColumnReorderProps<R>
|
|
22
|
+
): UseColumnReorderReturn {
|
|
23
|
+
const { columns, onColumnOrderChange, disableColumnReorder = false } = props;
|
|
24
|
+
|
|
25
|
+
const [draggedColumn, setDraggedColumn] = useState<string | null>(null);
|
|
26
|
+
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
|
|
27
|
+
const draggedIndexRef = useRef<number>(-1);
|
|
28
|
+
|
|
29
|
+
const handleDragStart = useCallback(
|
|
30
|
+
(field: string) => (event: React.DragEvent) => {
|
|
31
|
+
if (disableColumnReorder) return;
|
|
32
|
+
|
|
33
|
+
const columnIndex = columns.findIndex(col => col.field === field);
|
|
34
|
+
const column = columns[columnIndex];
|
|
35
|
+
|
|
36
|
+
if (column?.pinnable === false) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setDraggedColumn(field);
|
|
42
|
+
draggedIndexRef.current = columnIndex;
|
|
43
|
+
|
|
44
|
+
if (event.dataTransfer) {
|
|
45
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
46
|
+
event.dataTransfer.setData('text/plain', field);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[columns, disableColumnReorder]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const handleDragOver = useCallback(
|
|
53
|
+
(field: string) => (event: React.DragEvent) => {
|
|
54
|
+
if (disableColumnReorder || !draggedColumn) return;
|
|
55
|
+
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.dataTransfer.dropEffect = 'move';
|
|
58
|
+
|
|
59
|
+
setDragOverColumn(field);
|
|
60
|
+
},
|
|
61
|
+
[draggedColumn, disableColumnReorder]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const handleDrop = useCallback(
|
|
65
|
+
(targetField: string) => (event: React.DragEvent) => {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
|
|
68
|
+
if (disableColumnReorder || !draggedColumn || draggedColumn === targetField) {
|
|
69
|
+
setDraggedColumn(null);
|
|
70
|
+
setDragOverColumn(null);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const oldIndex = columns.findIndex(col => col.field === draggedColumn);
|
|
75
|
+
const targetIndex = columns.findIndex(col => col.field === targetField);
|
|
76
|
+
|
|
77
|
+
if (oldIndex === -1 || targetIndex === -1) {
|
|
78
|
+
setDraggedColumn(null);
|
|
79
|
+
setDragOverColumn(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const column = columns[oldIndex];
|
|
84
|
+
|
|
85
|
+
if (onColumnOrderChange) {
|
|
86
|
+
onColumnOrderChange({
|
|
87
|
+
column: column as any,
|
|
88
|
+
oldIndex,
|
|
89
|
+
targetIndex,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setDraggedColumn(null);
|
|
94
|
+
setDragOverColumn(null);
|
|
95
|
+
draggedIndexRef.current = -1;
|
|
96
|
+
},
|
|
97
|
+
[columns, draggedColumn, disableColumnReorder, onColumnOrderChange]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const handleDragEnd = useCallback(() => {
|
|
101
|
+
setDraggedColumn(null);
|
|
102
|
+
setDragOverColumn(null);
|
|
103
|
+
draggedIndexRef.current = -1;
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
draggedColumn,
|
|
108
|
+
dragOverColumn,
|
|
109
|
+
onDragStart: disableColumnReorder ? undefined : handleDragStart,
|
|
110
|
+
onDragOver: disableColumnReorder ? undefined : handleDragOver,
|
|
111
|
+
onDragEnd: disableColumnReorder ? undefined : handleDragEnd,
|
|
112
|
+
onDrop: disableColumnReorder ? undefined : handleDrop,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect, useCallback } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
GridRowModel,
|
|
4
|
+
GridRowId,
|
|
5
|
+
GridRowGroupingModel,
|
|
6
|
+
GridAggregationModel,
|
|
7
|
+
GridTreeNode,
|
|
8
|
+
GridFilterModel,
|
|
9
|
+
GridSortItem
|
|
10
|
+
} from '../types';
|
|
11
|
+
import { isRowMatchingFilter } from '../utils/filtering';
|
|
12
|
+
import { compareValues } from '../utils/sorting';
|
|
13
|
+
|
|
14
|
+
export interface UseRowGroupingParams<R extends GridRowModel> {
|
|
15
|
+
rows: R[];
|
|
16
|
+
getRowId: (row: R) => GridRowId;
|
|
17
|
+
rowGroupingModel?: GridRowGroupingModel;
|
|
18
|
+
aggregationModel?: GridAggregationModel;
|
|
19
|
+
defaultGroupingExpansionDepth?: number;
|
|
20
|
+
filterModel?: GridFilterModel;
|
|
21
|
+
sortModel?: GridSortItem[];
|
|
22
|
+
getAggregationPosition?: (groupNode: GridTreeNode | null) => 'inline' | 'footer' | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultGetAggregationPosition = (groupNode: GridTreeNode | null): 'inline' | 'footer' | null => {
|
|
26
|
+
|
|
27
|
+
return groupNode?.depth === -1 ? 'footer' : 'inline';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingParams<R>) {
|
|
31
|
+
const {
|
|
32
|
+
rows,
|
|
33
|
+
getRowId,
|
|
34
|
+
rowGroupingModel = [],
|
|
35
|
+
aggregationModel = {},
|
|
36
|
+
defaultGroupingExpansionDepth = 0,
|
|
37
|
+
filterModel,
|
|
38
|
+
sortModel,
|
|
39
|
+
getAggregationPosition = defaultGetAggregationPosition
|
|
40
|
+
} = params;
|
|
41
|
+
|
|
42
|
+
const [expandedGroupIds, setExpandedGroupIds] = useState<Set<GridRowId>>(new Set());
|
|
43
|
+
|
|
44
|
+
const { treeNodes, rootIds, groupingRows } = useMemo(() => {
|
|
45
|
+
const treeNodes = new Map<GridRowId, GridTreeNode>();
|
|
46
|
+
const groupingRows = new Map<GridRowId, R>();
|
|
47
|
+
const rootIds: GridRowId[] = [];
|
|
48
|
+
|
|
49
|
+
if (rowGroupingModel.length === 0) {
|
|
50
|
+
return { treeNodes, rootIds, groupingRows };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getGroupId = (field: string, value: any, parentId: GridRowId | null) => {
|
|
54
|
+
return `auto-group-${field}-${value}-${parentId || 'root'}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const groupRows = (
|
|
58
|
+
currentRows: R[],
|
|
59
|
+
depth: number,
|
|
60
|
+
parentId: GridRowId | null
|
|
61
|
+
) => {
|
|
62
|
+
if (depth >= rowGroupingModel.length) {
|
|
63
|
+
|
|
64
|
+
return currentRows.map(row => {
|
|
65
|
+
const id = getRowId(row);
|
|
66
|
+
treeNodes.set(id, {
|
|
67
|
+
id,
|
|
68
|
+
parentId,
|
|
69
|
+
depth,
|
|
70
|
+
groupingKey: '',
|
|
71
|
+
isExpanded: false
|
|
72
|
+
});
|
|
73
|
+
return id;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const field = rowGroupingModel[depth];
|
|
78
|
+
const groups = new Map<any, R[]>();
|
|
79
|
+
|
|
80
|
+
// Group current rows by value of the current field
|
|
81
|
+
currentRows.forEach(row => {
|
|
82
|
+
const value = row[field];
|
|
83
|
+
const key = String(value); // reliable map key
|
|
84
|
+
if (!groups.has(key)) {
|
|
85
|
+
groups.set(key, []);
|
|
86
|
+
}
|
|
87
|
+
groups.get(key)!.push(row);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const groupIds: GridRowId[] = [];
|
|
91
|
+
|
|
92
|
+
// Create group rows
|
|
93
|
+
groups.forEach((groupRowsList, key) => {
|
|
94
|
+
// Use the raw value from the first row of the group for the groupingValue
|
|
95
|
+
const firstRow = groupRowsList[0];
|
|
96
|
+
const rawValue = firstRow[field];
|
|
97
|
+
|
|
98
|
+
const groupId = getGroupId(field, key, parentId);
|
|
99
|
+
groupIds.push(groupId);
|
|
100
|
+
|
|
101
|
+
// Calculate Aggregations for this group
|
|
102
|
+
const aggregatedValues: Record<string, any> = {};
|
|
103
|
+
if (aggregationModel) {
|
|
104
|
+
Object.entries(aggregationModel).forEach(([aggField, aggType]) => {
|
|
105
|
+
const values = groupRowsList.map(r => r[aggField]);
|
|
106
|
+
if (aggType === 'sum') {
|
|
107
|
+
aggregatedValues[aggField] = values.reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0);
|
|
108
|
+
} else if (aggType === 'min') {
|
|
109
|
+
aggregatedValues[aggField] = Math.min(...values.map(v => Number(v) || 0));
|
|
110
|
+
} else if (aggType === 'max') {
|
|
111
|
+
aggregatedValues[aggField] = Math.max(...values.map(v => Number(v) || 0));
|
|
112
|
+
} else if (aggType === 'avg') {
|
|
113
|
+
const sum = values.reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0);
|
|
114
|
+
aggregatedValues[aggField] = values.length ? sum / values.length : 0;
|
|
115
|
+
} else if (aggType === 'count') {
|
|
116
|
+
aggregatedValues[aggField] = values.length;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const groupRow: any = {
|
|
122
|
+
[field]: rawValue,
|
|
123
|
+
...aggregatedValues,
|
|
124
|
+
id: groupId
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
groupingRows.set(groupId, groupRow);
|
|
128
|
+
|
|
129
|
+
const treeNode: GridTreeNode = {
|
|
130
|
+
id: groupId,
|
|
131
|
+
parentId,
|
|
132
|
+
depth,
|
|
133
|
+
groupingKey: key,
|
|
134
|
+
groupingField: field,
|
|
135
|
+
groupingValue: rawValue,
|
|
136
|
+
aggregatedValues,
|
|
137
|
+
isExpanded: false,
|
|
138
|
+
children: [],
|
|
139
|
+
label: `${field}: ${String(rawValue)}`
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
treeNode.aggregationPosition = getAggregationPosition(treeNode);
|
|
143
|
+
|
|
144
|
+
treeNodes.set(groupId, treeNode);
|
|
145
|
+
|
|
146
|
+
const childrenIds = groupRows(groupRowsList, depth + 1, groupId);
|
|
147
|
+
treeNodes.get(groupId)!.children = childrenIds;
|
|
148
|
+
|
|
149
|
+
const descendantCount = groupRowsList.length;
|
|
150
|
+
treeNodes.get(groupId)!.descendantCount = descendantCount;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return groupIds;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const topLevelIds = groupRows(rows, 0, null);
|
|
157
|
+
rootIds.push(...topLevelIds);
|
|
158
|
+
|
|
159
|
+
return { treeNodes, rootIds, groupingRows };
|
|
160
|
+
|
|
161
|
+
}, [rows, rowGroupingModel, aggregationModel, getRowId, getAggregationPosition]);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (defaultGroupingExpansionDepth === -1) {
|
|
165
|
+
|
|
166
|
+
const allGroupIds = new Set<GridRowId>();
|
|
167
|
+
treeNodes.forEach((node, id) => {
|
|
168
|
+
if (node.children && node.children.length > 0) {
|
|
169
|
+
allGroupIds.add(id);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
setExpandedGroupIds(allGroupIds);
|
|
173
|
+
} else if (defaultGroupingExpansionDepth > 0) {
|
|
174
|
+
const depthIds = new Set<GridRowId>();
|
|
175
|
+
treeNodes.forEach((node, id) => {
|
|
176
|
+
if (node.depth < defaultGroupingExpansionDepth && node.children && node.children.length > 0) {
|
|
177
|
+
depthIds.add(id);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
setExpandedGroupIds(depthIds);
|
|
181
|
+
} else {
|
|
182
|
+
|
|
183
|
+
setExpandedGroupIds(new Set());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
}, [rowGroupingModel, defaultGroupingExpansionDepth]);
|
|
187
|
+
|
|
188
|
+
const toggleExpansion = useCallback((id: GridRowId) => {
|
|
189
|
+
setExpandedGroupIds(prev => {
|
|
190
|
+
const next = new Set(prev);
|
|
191
|
+
if (next.has(id)) {
|
|
192
|
+
next.delete(id);
|
|
193
|
+
} else {
|
|
194
|
+
next.add(id);
|
|
195
|
+
}
|
|
196
|
+
return next;
|
|
197
|
+
});
|
|
198
|
+
}, []);
|
|
199
|
+
|
|
200
|
+
const isGroupExpanded = useCallback((id: GridRowId) => {
|
|
201
|
+
return expandedGroupIds.has(id);
|
|
202
|
+
}, [expandedGroupIds]);
|
|
203
|
+
|
|
204
|
+
const getVisibleRows = useCallback(() => {
|
|
205
|
+
if (rowGroupingModel.length === 0) return null;
|
|
206
|
+
|
|
207
|
+
const result: R[] = [];
|
|
208
|
+
const seenIds = new Set<GridRowId>();
|
|
209
|
+
|
|
210
|
+
const rowLookup = new Map<GridRowId, R>();
|
|
211
|
+
rows.forEach(r => rowLookup.set(getRowId(r), r));
|
|
212
|
+
|
|
213
|
+
const doesNodeMatchFilter = (nodeId: GridRowId): boolean => {
|
|
214
|
+
const node = treeNodes.get(nodeId);
|
|
215
|
+
if (!node) return false;
|
|
216
|
+
|
|
217
|
+
if (!node.children || node.children.length === 0) {
|
|
218
|
+
|
|
219
|
+
const row = rowLookup.get(nodeId);
|
|
220
|
+
if (!row) return false;
|
|
221
|
+
|
|
222
|
+
if (filterModel) {
|
|
223
|
+
if (!isRowMatchingFilter(row, filterModel)) return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return node.children.some(childId => doesNodeMatchFilter(childId));
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const traverse = (ids: GridRowId[]) => {
|
|
232
|
+
|
|
233
|
+
let visibleIds = ids;
|
|
234
|
+
if (filterModel) {
|
|
235
|
+
visibleIds = ids.filter(doesNodeMatchFilter);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (sortModel && sortModel.length > 0) {
|
|
239
|
+
visibleIds.sort((aId, bId) => {
|
|
240
|
+
const rowA = groupingRows.get(aId) || rowLookup.get(aId);
|
|
241
|
+
const rowB = groupingRows.get(bId) || rowLookup.get(bId);
|
|
242
|
+
|
|
243
|
+
if (!rowA || !rowB) return 0;
|
|
244
|
+
|
|
245
|
+
for (const sortItem of sortModel) {
|
|
246
|
+
const valA = (rowA as any)[sortItem.field];
|
|
247
|
+
const valB = (rowB as any)[sortItem.field];
|
|
248
|
+
const compareResult = compareValues(valA, valB, sortItem.sort);
|
|
249
|
+
if (compareResult !== 0) return compareResult;
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
visibleIds.forEach(id => {
|
|
256
|
+
if (seenIds.has(id)) return;
|
|
257
|
+
|
|
258
|
+
const node = treeNodes.get(id);
|
|
259
|
+
const isGroup = node?.children && node.children.length > 0;
|
|
260
|
+
|
|
261
|
+
const row = isGroup ? groupingRows.get(id) : rowLookup.get(id);
|
|
262
|
+
|
|
263
|
+
if (row && node) {
|
|
264
|
+
seenIds.add(id);
|
|
265
|
+
|
|
266
|
+
const enhancedRow = {
|
|
267
|
+
...row,
|
|
268
|
+
_treeDepth: node.depth,
|
|
269
|
+
_isExpanded: expandedGroupIds.has(id),
|
|
270
|
+
_hasChildren: isGroup,
|
|
271
|
+
_groupingField: node.groupingField,
|
|
272
|
+
_groupingValue: node.groupingValue,
|
|
273
|
+
_descendantCount: node.descendantCount
|
|
274
|
+
};
|
|
275
|
+
result.push(enhancedRow);
|
|
276
|
+
|
|
277
|
+
if (isExpandedGroupIds.has(id) && isGroup) {
|
|
278
|
+
traverse(node.children!);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const isExpandedGroupIds = expandedGroupIds;
|
|
285
|
+
|
|
286
|
+
traverse(rootIds);
|
|
287
|
+
|
|
288
|
+
return result;
|
|
289
|
+
|
|
290
|
+
}, [rows, getRowId, treeNodes, rootIds, groupingRows, filterModel, sortModel, expandedGroupIds]);
|
|
291
|
+
|
|
292
|
+
const getNode = useCallback((id: GridRowId) => treeNodes.get(id), [treeNodes]);
|
|
293
|
+
|
|
294
|
+
return useMemo(() => ({
|
|
295
|
+
treeNodes,
|
|
296
|
+
groupingRows,
|
|
297
|
+
toggleExpansion,
|
|
298
|
+
isGroupExpanded,
|
|
299
|
+
getVisibleRows,
|
|
300
|
+
getNode
|
|
301
|
+
}), [treeNodes, groupingRows, toggleExpansion, isGroupExpanded, getVisibleRows, getNode]);
|
|
302
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import type { GridRowId, GridRowModel, GridRowOrderChangeParams } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface UseRowReorderProps<R extends GridRowModel = GridRowModel> {
|
|
5
|
+
onRowOrderChange?: (params: GridRowOrderChangeParams<R>) => void;
|
|
6
|
+
rows: R[];
|
|
7
|
+
getRowId?: (row: R) => GridRowId;
|
|
8
|
+
rowReordering?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseRowReorderReturn {
|
|
12
|
+
draggedRowId: GridRowId | null;
|
|
13
|
+
dragOverRowId: GridRowId | null;
|
|
14
|
+
onDragStart: ((id: GridRowId) => (event: React.DragEvent) => void) | undefined;
|
|
15
|
+
onDragOver: ((id: GridRowId) => (event: React.DragEvent) => void) | undefined;
|
|
16
|
+
onDragEnd: (() => void) | undefined;
|
|
17
|
+
onDrop: ((targetId: GridRowId) => (event: React.DragEvent) => void) | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useRowReorder<R extends GridRowModel = GridRowModel>(
|
|
21
|
+
props: UseRowReorderProps<R>
|
|
22
|
+
): UseRowReorderReturn {
|
|
23
|
+
const { onRowOrderChange, rows, getRowId, rowReordering = false } = props;
|
|
24
|
+
const [draggedRowId, setDraggedRowId] = useState<GridRowId | null>(null);
|
|
25
|
+
const [dragOverRowId, setDragOverRowId] = useState<GridRowId | null>(null);
|
|
26
|
+
|
|
27
|
+
const getRowIdInternal = useCallback((row: R) => {
|
|
28
|
+
return getRowId ? getRowId(row) : row.id;
|
|
29
|
+
}, [getRowId]);
|
|
30
|
+
|
|
31
|
+
const handleDragStart = useCallback((id: GridRowId) => (event: React.DragEvent) => {
|
|
32
|
+
if (!rowReordering) return;
|
|
33
|
+
setDraggedRowId(id);
|
|
34
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
35
|
+
|
|
36
|
+
}, [rowReordering]);
|
|
37
|
+
|
|
38
|
+
const handleDragOver = useCallback((id: GridRowId) => (event: React.DragEvent) => {
|
|
39
|
+
if (!rowReordering || !draggedRowId) return;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
event.dataTransfer.dropEffect = 'move';
|
|
42
|
+
|
|
43
|
+
if (dragOverRowId !== id) {
|
|
44
|
+
setDragOverRowId(id);
|
|
45
|
+
}
|
|
46
|
+
}, [rowReordering, draggedRowId, dragOverRowId]);
|
|
47
|
+
|
|
48
|
+
const handleDragEnd = useCallback(() => {
|
|
49
|
+
setDraggedRowId(null);
|
|
50
|
+
setDragOverRowId(null);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleDrop = useCallback((targetId: GridRowId) => (event: React.DragEvent) => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
|
|
56
|
+
if (!draggedRowId || !onRowOrderChange) {
|
|
57
|
+
handleDragEnd();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const oldIndex = rows.findIndex(r => getRowIdInternal(r) === draggedRowId);
|
|
62
|
+
const targetIndex = rows.findIndex(r => getRowIdInternal(r) === targetId);
|
|
63
|
+
|
|
64
|
+
if (oldIndex !== -1 && targetIndex !== -1 && oldIndex !== targetIndex) {
|
|
65
|
+
onRowOrderChange({
|
|
66
|
+
row: rows[oldIndex],
|
|
67
|
+
oldIndex,
|
|
68
|
+
targetIndex
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handleDragEnd();
|
|
73
|
+
}, [draggedRowId, onRowOrderChange, rows, getRowIdInternal, handleDragEnd]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
draggedRowId,
|
|
77
|
+
dragOverRowId,
|
|
78
|
+
onDragStart: rowReordering ? handleDragStart : undefined,
|
|
79
|
+
onDragOver: rowReordering ? handleDragOver : undefined,
|
|
80
|
+
onDragEnd: rowReordering ? handleDragEnd : undefined,
|
|
81
|
+
onDrop: rowReordering ? handleDrop : undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|