argent-grid 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +70 -27
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +112 -0
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +21 -7
- package/plan.md +56 -28
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +281 -321
- package/src/lib/components/argent-grid.component.html +295 -207
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +1193 -290
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +307 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
- package/src/lib/rendering/canvas-renderer.ts +456 -452
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +167 -28
- package/src/lib/rendering/render/column-utils.ts +95 -0
- package/src/lib/rendering/render/hit-test.ts +50 -0
- package/src/lib/rendering/render/index.ts +88 -76
- package/src/lib/rendering/render/lines.ts +53 -47
- package/src/lib/rendering/render/primitives.ts +423 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +3 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +94 -64
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1241 -201
- package/src/lib/services/grid.service.ts +1204 -235
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +573 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +249 -0
- package/src/stories/ArgentGrid.stories.ts +301 -0
- package/src/stories/Benchmark.stories.ts +76 -0
- package/src/stories/CellRenderers.stories.ts +395 -0
- package/src/stories/Filtering.stories.ts +292 -0
- package/src/stories/Grouping.stories.ts +290 -0
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +137 -0
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +355 -0
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- package/demo-app/tsconfig.json +0 -31
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { moveItemInArray } from '@angular/cdk/drag-drop';
|
|
2
|
+
import { Injectable } from '@angular/core';
|
|
3
3
|
import { Workbook } from 'exceljs';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { Subject } from 'rxjs';
|
|
5
|
+
import {
|
|
6
|
+
CellRange,
|
|
6
7
|
ColDef,
|
|
7
8
|
ColGroupDef,
|
|
8
9
|
Column,
|
|
9
|
-
|
|
10
|
+
ColumnGroup,
|
|
11
|
+
CsvExportParams,
|
|
12
|
+
ExcelExportParams,
|
|
10
13
|
FilterModel,
|
|
11
14
|
FilterModelItem,
|
|
12
|
-
|
|
15
|
+
GridApi,
|
|
16
|
+
GridOptions,
|
|
13
17
|
GridState,
|
|
18
|
+
GroupRowNode,
|
|
19
|
+
IRowNode,
|
|
14
20
|
RowDataTransaction,
|
|
15
21
|
RowDataTransactionResult,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
GroupRowNode,
|
|
19
|
-
CellRange
|
|
22
|
+
SortDirection,
|
|
23
|
+
SortModelItem,
|
|
20
24
|
} from '../types/ag-grid-types';
|
|
21
25
|
|
|
22
26
|
@Injectable()
|
|
23
27
|
export class GridService<TData = any> {
|
|
24
28
|
private columns: Map<string, Column> = new Map();
|
|
29
|
+
private columnGroups: ColumnGroup[] = [];
|
|
30
|
+
private headerDepth = 1;
|
|
25
31
|
private rowData: TData[] = [];
|
|
26
32
|
private rowNodes: Map<string, IRowNode<TData>> = new Map();
|
|
27
33
|
private displayedRowNodes: IRowNode<TData>[] = [];
|
|
@@ -34,7 +40,12 @@ export class GridService<TData = any> {
|
|
|
34
40
|
private cellRanges: CellRange[] = [];
|
|
35
41
|
private gridId: string = '';
|
|
36
42
|
private gridOptions: GridOptions<TData> | null = null;
|
|
37
|
-
public gridStateChanged$ = new Subject<{
|
|
43
|
+
public gridStateChanged$ = new Subject<{
|
|
44
|
+
type: string;
|
|
45
|
+
key?: string;
|
|
46
|
+
value?: any;
|
|
47
|
+
changedRowIndices?: number[];
|
|
48
|
+
}>();
|
|
38
49
|
|
|
39
50
|
// Row height cache
|
|
40
51
|
private cumulativeRowHeights: number[] = [];
|
|
@@ -47,7 +58,7 @@ export class GridService<TData = any> {
|
|
|
47
58
|
// Pivoting state
|
|
48
59
|
private pivotColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
|
|
49
60
|
private isPivotMode = false;
|
|
50
|
-
|
|
61
|
+
|
|
51
62
|
createApi(
|
|
52
63
|
columnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null,
|
|
53
64
|
rowData: TData[] | null,
|
|
@@ -61,32 +72,78 @@ export class GridService<TData = any> {
|
|
|
61
72
|
this.gridOptions = gridOptions ? { ...gridOptions } : {};
|
|
62
73
|
this.isPivotMode = !!this.gridOptions.pivotMode;
|
|
63
74
|
|
|
64
|
-
this.initializeColumns();
|
|
65
|
-
|
|
75
|
+
this.initializeColumns(true);
|
|
76
|
+
|
|
66
77
|
// Trigger initial pipeline run
|
|
67
78
|
this.applySorting();
|
|
68
79
|
this.applyFiltering(); // This will trigger grouping if needed and initialize nodes
|
|
69
80
|
|
|
70
81
|
return this.createGridApi();
|
|
71
82
|
}
|
|
72
|
-
|
|
83
|
+
|
|
73
84
|
private generateGridId(): string {
|
|
74
85
|
return `argent-grid-${Math.random().toString(36).substr(2, 9)}`;
|
|
75
86
|
}
|
|
76
|
-
|
|
77
|
-
private
|
|
87
|
+
|
|
88
|
+
private hasCheckboxSelection(): boolean {
|
|
89
|
+
const rowSelection = this.gridOptions?.rowSelection;
|
|
90
|
+
const isMultiMode =
|
|
91
|
+
rowSelection === 'multiple' ||
|
|
92
|
+
(typeof rowSelection === 'object' && rowSelection.mode === 'multiRow');
|
|
93
|
+
|
|
94
|
+
if (isMultiMode) return true;
|
|
95
|
+
if (this.gridOptions?.selectionColumnDef?.checkboxes) return true;
|
|
96
|
+
if (!this.columnDefs) return false;
|
|
97
|
+
|
|
98
|
+
const check = (defs: (ColDef | ColGroupDef)[]): boolean => {
|
|
99
|
+
return defs.some((def) => {
|
|
100
|
+
if ('children' in def) return check(def.children);
|
|
101
|
+
return !!def.checkboxSelection;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return check(this.columnDefs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private initializeColumns(clearColumns: boolean = true): void {
|
|
78
109
|
if (!this.columnDefs) {
|
|
79
110
|
return;
|
|
80
111
|
}
|
|
81
|
-
|
|
112
|
+
|
|
113
|
+
const existingColumns = clearColumns ? [] : Array.from(this.columns.values());
|
|
82
114
|
this.columns.clear();
|
|
115
|
+
this.columnGroups = [];
|
|
116
|
+
this.headerDepth = 1;
|
|
83
117
|
|
|
84
118
|
const groupColumns = this.getGroupColumns();
|
|
85
119
|
const isGrouping = groupColumns.length > 0;
|
|
86
120
|
const groupDisplayType = this.gridOptions?.groupDisplayType || 'singleColumn';
|
|
87
121
|
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
const topLevelColumns: (Column | ColumnGroup)[] = [];
|
|
123
|
+
|
|
124
|
+
// 1. Handle Selection Column
|
|
125
|
+
if (this.hasCheckboxSelection()) {
|
|
126
|
+
const selectionCol: Column = {
|
|
127
|
+
colId: 'ag-Grid-SelectionColumn',
|
|
128
|
+
field: 'ag-Grid-SelectionColumn',
|
|
129
|
+
headerName: '',
|
|
130
|
+
width: 50,
|
|
131
|
+
pinned: 'left',
|
|
132
|
+
visible: true,
|
|
133
|
+
sort: null,
|
|
134
|
+
checkboxSelection: true,
|
|
135
|
+
headerCheckboxSelection: true,
|
|
136
|
+
colIndex: 0,
|
|
137
|
+
};
|
|
138
|
+
this.columns.set(selectionCol.colId, selectionCol);
|
|
139
|
+
topLevelColumns.push(selectionCol);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. Handle Auto Group Column (for singleColumn display)
|
|
143
|
+
if (
|
|
144
|
+
isGrouping &&
|
|
145
|
+
(groupDisplayType === 'singleColumn' || !this.gridOptions?.groupDisplayType)
|
|
146
|
+
) {
|
|
90
147
|
const autoGroupDef = this.gridOptions?.autoGroupColumnDef || {};
|
|
91
148
|
const autoGroupCol: Column = {
|
|
92
149
|
colId: 'ag-Grid-AutoColumn',
|
|
@@ -97,74 +154,387 @@ export class GridService<TData = any> {
|
|
|
97
154
|
maxWidth: autoGroupDef.maxWidth,
|
|
98
155
|
pinned: this.normalizePinned(autoGroupDef.pinned || 'left'),
|
|
99
156
|
visible: true,
|
|
100
|
-
sort: null
|
|
157
|
+
sort: null,
|
|
158
|
+
colIndex: this.columns.size,
|
|
101
159
|
};
|
|
102
160
|
this.columns.set(autoGroupCol.colId, autoGroupCol);
|
|
161
|
+
topLevelColumns.push(autoGroupCol);
|
|
103
162
|
}
|
|
104
163
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
164
|
+
// 3. Process columns
|
|
165
|
+
let currentLeafIndex = topLevelColumns.length;
|
|
166
|
+
|
|
167
|
+
const processDefs = (
|
|
168
|
+
defs: (ColDef<TData> | ColGroupDef<TData>)[],
|
|
169
|
+
level: number,
|
|
170
|
+
parent?: ColumnGroup
|
|
171
|
+
): (Column | ColumnGroup)[] => {
|
|
172
|
+
return defs.map((def, index) => {
|
|
173
|
+
if ('children' in def) {
|
|
174
|
+
const groupId = def.groupId || `group-${index}-${level}`;
|
|
175
|
+
const group: ColumnGroup = {
|
|
176
|
+
groupId,
|
|
177
|
+
headerName: def.headerName,
|
|
178
|
+
children: [],
|
|
179
|
+
displayedChildren: [],
|
|
180
|
+
visible: true,
|
|
181
|
+
expanded: !!def.openByDefault,
|
|
182
|
+
parent,
|
|
183
|
+
level,
|
|
184
|
+
pinned: false,
|
|
185
|
+
columnGroupShow: def.columnGroupShow,
|
|
186
|
+
marryChildren: !!def.marryChildren,
|
|
187
|
+
colIndex: 0,
|
|
188
|
+
};
|
|
109
189
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
190
|
+
const startIndex = currentLeafIndex;
|
|
191
|
+
group.children = processDefs(def.children, level + 1, group);
|
|
192
|
+
group.colIndex = startIndex;
|
|
193
|
+
|
|
194
|
+
const firstPinned = group.children.find((c) => 'pinned' in c && c.pinned)?.pinned;
|
|
195
|
+
group.pinned = firstPinned || false;
|
|
196
|
+
|
|
197
|
+
this.columnGroups.push(group);
|
|
198
|
+
return group;
|
|
199
|
+
} else {
|
|
200
|
+
const mergedDef = { ...this.gridOptions?.defaultColDef, ...def };
|
|
201
|
+
const colId = mergedDef.colId || mergedDef.field?.toString() || `col-${index}-${level}`;
|
|
202
|
+
|
|
203
|
+
// Preserve existing column if it exists to maintain width/order etc.
|
|
204
|
+
let column = existingColumns.find((c) => c.colId === colId);
|
|
205
|
+
|
|
206
|
+
let visible = !mergedDef.hide;
|
|
207
|
+
if (
|
|
208
|
+
isGrouping &&
|
|
209
|
+
mergedDef.rowGroup &&
|
|
210
|
+
visible &&
|
|
211
|
+
this.gridOptions?.groupHideOpenParents !== false
|
|
212
|
+
) {
|
|
213
|
+
visible = false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (this.isPivotMode && mergedDef.pivot && visible) {
|
|
217
|
+
visible = false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (column) {
|
|
221
|
+
column.visible = visible;
|
|
222
|
+
column.parent = parent;
|
|
223
|
+
column.colIndex = currentLeafIndex++;
|
|
224
|
+
return column;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
column = {
|
|
228
|
+
colId,
|
|
229
|
+
field: mergedDef.field?.toString(),
|
|
230
|
+
headerName: mergedDef.headerName,
|
|
231
|
+
width: mergedDef.width || 150,
|
|
232
|
+
minWidth: mergedDef.minWidth,
|
|
233
|
+
maxWidth: mergedDef.maxWidth,
|
|
234
|
+
pinned: this.normalizePinned(mergedDef.pinned),
|
|
235
|
+
visible: visible,
|
|
236
|
+
sort:
|
|
237
|
+
typeof mergedDef.sort === 'object' && mergedDef.sort !== null
|
|
238
|
+
? (mergedDef.sort as any).sort
|
|
239
|
+
: mergedDef.sort || null,
|
|
240
|
+
sortIndex: mergedDef.sortIndex ?? undefined,
|
|
241
|
+
aggFunc: typeof mergedDef.aggFunc === 'string' ? mergedDef.aggFunc : null,
|
|
242
|
+
checkboxSelection: !!mergedDef.checkboxSelection,
|
|
243
|
+
headerCheckboxSelection: !!mergedDef.headerCheckboxSelection,
|
|
244
|
+
filter: mergedDef.filter,
|
|
245
|
+
parent,
|
|
246
|
+
columnGroupShow: mergedDef.columnGroupShow,
|
|
247
|
+
colIndex: currentLeafIndex++,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
this.columns.set(colId, column);
|
|
251
|
+
return column;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const defsToProcess =
|
|
257
|
+
this.isPivotMode && this.pivotColumnDefs ? this.pivotColumnDefs : this.columnDefs || [];
|
|
258
|
+
|
|
259
|
+
topLevelColumns.push(...processDefs(defsToProcess, 0));
|
|
260
|
+
|
|
261
|
+
// 4. Add back any columns that were in existingColumns but not in defs
|
|
262
|
+
// (Only for normal columns, not internal Selection or AutoGroup columns
|
|
263
|
+
// which are handled in steps 1 and 2 above)
|
|
264
|
+
existingColumns.forEach((c) => {
|
|
265
|
+
if (
|
|
266
|
+
c.colId !== 'ag-Grid-SelectionColumn' &&
|
|
267
|
+
c.colId !== 'ag-Grid-AutoColumn' &&
|
|
268
|
+
!this.columns.has(c.colId)
|
|
269
|
+
) {
|
|
270
|
+
this.columns.set(c.colId, c);
|
|
118
271
|
}
|
|
119
272
|
});
|
|
273
|
+
|
|
274
|
+
// 5. Calculate header depth
|
|
275
|
+
this.headerDepth = this.calculateHeaderDepth(topLevelColumns);
|
|
120
276
|
}
|
|
121
277
|
|
|
122
|
-
private
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
278
|
+
private calculateHeaderDepth(columns: (Column | ColumnGroup)[]): number {
|
|
279
|
+
let maxDepth = 0;
|
|
280
|
+
const walk = (items: (Column | ColumnGroup)[], depth: number) => {
|
|
281
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
282
|
+
items.forEach((item) => {
|
|
283
|
+
if ('children' in item) {
|
|
284
|
+
walk(item.children, depth + 1);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
walk(columns, 1);
|
|
289
|
+
return maxDepth;
|
|
126
290
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let visible = !def.hide;
|
|
133
|
-
if (isGrouping && def.rowGroup && visible && this.gridOptions?.groupHideOpenParents !== false) {
|
|
134
|
-
visible = false;
|
|
291
|
+
|
|
292
|
+
public getHeaderRows(): (Column | ColumnGroup)[][] {
|
|
293
|
+
const rows: (Column | ColumnGroup)[][] = [];
|
|
294
|
+
for (let i = 0; i < this.headerDepth; i++) {
|
|
295
|
+
rows[i] = [];
|
|
135
296
|
}
|
|
136
297
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
298
|
+
const walk = (items: (Column | ColumnGroup)[], level: number) => {
|
|
299
|
+
items.forEach((item) => {
|
|
300
|
+
rows[level].push(item);
|
|
301
|
+
if ('children' in item) {
|
|
302
|
+
walk(item.children, level + 1);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const topItems: (Column | ColumnGroup)[] = [];
|
|
308
|
+
|
|
309
|
+
if (this.hasCheckboxSelection()) {
|
|
310
|
+
topItems.push(this.columns.get('ag-Grid-SelectionColumn')!);
|
|
140
311
|
}
|
|
141
312
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
313
|
+
const isGrouping = this.getGroupColumns().length > 0;
|
|
314
|
+
const groupDisplayType = this.gridOptions?.groupDisplayType || 'singleColumn';
|
|
315
|
+
if (
|
|
316
|
+
isGrouping &&
|
|
317
|
+
(groupDisplayType === 'singleColumn' || !this.gridOptions?.groupDisplayType)
|
|
318
|
+
) {
|
|
319
|
+
topItems.push(this.columns.get('ag-Grid-AutoColumn')!);
|
|
145
320
|
}
|
|
146
321
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
322
|
+
const defsToProcess =
|
|
323
|
+
this.isPivotMode && this.pivotColumnDefs ? this.pivotColumnDefs : this.columnDefs || [];
|
|
324
|
+
|
|
325
|
+
const processTop = (
|
|
326
|
+
defs: (ColDef<TData> | ColGroupDef<TData>)[],
|
|
327
|
+
level: number
|
|
328
|
+
): (Column | ColumnGroup)[] => {
|
|
329
|
+
return defs.map((def, index) => {
|
|
330
|
+
if ('children' in def) {
|
|
331
|
+
const groupId = def.groupId || `group-${index}-${level}`;
|
|
332
|
+
return this.columnGroups.find((g) => g.groupId === groupId)!;
|
|
333
|
+
} else {
|
|
334
|
+
const colId = def.colId || def.field?.toString() || `col-${index}-${level}`;
|
|
335
|
+
return this.columns.get(colId)!;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
topItems.push(...processTop(defsToProcess, 0));
|
|
341
|
+
|
|
342
|
+
walk(topItems, 0);
|
|
343
|
+
return rows;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
public toggleColumnGroup(groupId: string, expanded: boolean): void {
|
|
347
|
+
const group = this.columnGroups.find((g) => g.groupId === groupId);
|
|
348
|
+
if (group) {
|
|
349
|
+
group.expanded = expanded;
|
|
350
|
+
this.gridStateChanged$.next({ type: 'columnGroupExpanded', key: groupId, value: expanded });
|
|
150
351
|
}
|
|
352
|
+
}
|
|
151
353
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
354
|
+
public addRowGroupColumn(colId: string): void {
|
|
355
|
+
if (!this.columnDefs) return;
|
|
356
|
+
|
|
357
|
+
const updateDef = (defs: (ColDef<TData> | ColGroupDef<TData>)[]) => {
|
|
358
|
+
defs.forEach((def) => {
|
|
359
|
+
if ('children' in def) {
|
|
360
|
+
updateDef(def.children);
|
|
361
|
+
} else if (def.colId === colId || def.field === colId) {
|
|
362
|
+
def.rowGroup = true;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
this.expandedGroups.clear();
|
|
368
|
+
updateDef(this.columnDefs);
|
|
369
|
+
this.initializeColumns(false);
|
|
370
|
+
this.applyFiltering();
|
|
371
|
+
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
public removeRowGroupColumn(colId: string): void {
|
|
375
|
+
if (!this.columnDefs) return;
|
|
376
|
+
|
|
377
|
+
const updateDef = (defs: (ColDef<TData> | ColGroupDef<TData>)[]) => {
|
|
378
|
+
defs.forEach((def) => {
|
|
379
|
+
if ('children' in def) {
|
|
380
|
+
updateDef(def.children);
|
|
381
|
+
} else if (def.colId === colId || def.field === colId) {
|
|
382
|
+
def.rowGroup = false;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
this.expandedGroups.clear();
|
|
388
|
+
updateDef(this.columnDefs);
|
|
389
|
+
this.initializeColumns(false);
|
|
390
|
+
this.applyFiltering();
|
|
391
|
+
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
public setRowGroupColumns(colIds: string[]): void {
|
|
395
|
+
if (!this.columnDefs) return;
|
|
396
|
+
|
|
397
|
+
const updateDef = (defs: (ColDef<TData> | ColGroupDef<TData>)[]) => {
|
|
398
|
+
defs.forEach((def) => {
|
|
399
|
+
if ('children' in def) {
|
|
400
|
+
updateDef(def.children);
|
|
401
|
+
} else {
|
|
402
|
+
def.rowGroup = colIds.includes(def.colId || def.field?.toString() || '');
|
|
403
|
+
}
|
|
404
|
+
});
|
|
164
405
|
};
|
|
165
|
-
|
|
406
|
+
|
|
407
|
+
this.expandedGroups.clear();
|
|
408
|
+
updateDef(this.columnDefs);
|
|
409
|
+
this.initializeColumns(false);
|
|
410
|
+
this.applyFiltering();
|
|
411
|
+
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
166
412
|
}
|
|
167
|
-
|
|
413
|
+
|
|
414
|
+
public setColumnPinned(col: string | Column, pinned: 'left' | 'right' | boolean): void {
|
|
415
|
+
const colId = typeof col === 'string' ? col : col.colId;
|
|
416
|
+
const column = this.columns.get(colId);
|
|
417
|
+
if (column) {
|
|
418
|
+
column.pinned = this.normalizePinned(pinned);
|
|
419
|
+
|
|
420
|
+
// Also update ColDef
|
|
421
|
+
if (this.columnDefs) {
|
|
422
|
+
const updateDef = (defs: (ColDef<TData> | ColGroupDef<TData>)[]) => {
|
|
423
|
+
defs.forEach((def) => {
|
|
424
|
+
if ('children' in def) updateDef(def.children);
|
|
425
|
+
else if (def.colId === colId || def.field === colId) {
|
|
426
|
+
def.pinned = column.pinned === false ? null : column.pinned;
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
updateDef(this.columnDefs);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.initializeColumns(false);
|
|
434
|
+
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
public moveColumn(col: string | Column, toIndex: number): void {
|
|
439
|
+
const colId = typeof col === 'string' ? col : col.colId;
|
|
440
|
+
const column = this.columns.get(colId);
|
|
441
|
+
|
|
442
|
+
if (column) {
|
|
443
|
+
const allCols = Array.from(this.columns.values());
|
|
444
|
+
const fromIdx = allCols.findIndex((c) => c.colId === colId);
|
|
445
|
+
|
|
446
|
+
// We want to find the target position relative to the section
|
|
447
|
+
const sectionCols = allCols.filter((c) => c.pinned === column.pinned);
|
|
448
|
+
const targetCol = sectionCols[toIndex];
|
|
449
|
+
|
|
450
|
+
let toIdx = -1;
|
|
451
|
+
if (targetCol) {
|
|
452
|
+
toIdx = allCols.findIndex((c) => c.colId === targetCol.colId);
|
|
453
|
+
} else {
|
|
454
|
+
// Drop at end of section
|
|
455
|
+
if (toIndex >= sectionCols.length) {
|
|
456
|
+
const lastInSection = sectionCols[sectionCols.length - 1];
|
|
457
|
+
if (lastInSection) {
|
|
458
|
+
toIdx = allCols.findIndex((c) => c.colId === lastInSection.colId);
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
const firstInSection = sectionCols[0];
|
|
462
|
+
if (firstInSection) {
|
|
463
|
+
toIdx = allCols.findIndex((c) => c.colId === firstInSection.colId);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (toIdx !== -1 && fromIdx !== toIdx) {
|
|
469
|
+
moveItemInArray(allCols, fromIdx, toIdx);
|
|
470
|
+
|
|
471
|
+
// Also move in columnDefs if present
|
|
472
|
+
if (this.columnDefs) {
|
|
473
|
+
const fromDefIdx = this.columnDefs.findIndex(
|
|
474
|
+
(d) => !('children' in d) && (d.colId === colId || d.field?.toString() === colId)
|
|
475
|
+
);
|
|
476
|
+
if (fromDefIdx !== -1 && targetCol) {
|
|
477
|
+
const toDefIdx = this.columnDefs.findIndex(
|
|
478
|
+
(d) =>
|
|
479
|
+
!('children' in d) &&
|
|
480
|
+
(d.colId === targetCol.colId || d.field?.toString() === targetCol.colId)
|
|
481
|
+
);
|
|
482
|
+
if (toDefIdx !== -1) {
|
|
483
|
+
moveItemInArray(this.columnDefs, fromDefIdx, toDefIdx);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
this.columns.clear();
|
|
489
|
+
allCols.forEach((c) => this.columns.set(c.colId, c));
|
|
490
|
+
|
|
491
|
+
this.initializeColumns(false);
|
|
492
|
+
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
public setColumnVisible(col: string | Column, visible: boolean): void {
|
|
498
|
+
const colId = typeof col === 'string' ? col : col.colId;
|
|
499
|
+
const column = this.columns.get(colId);
|
|
500
|
+
if (column) {
|
|
501
|
+
column.visible = visible;
|
|
502
|
+
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
public setColumnWidth(col: string | Column, width: number): void {
|
|
507
|
+
const colId = typeof col === 'string' ? col : col.colId;
|
|
508
|
+
const column = this.columns.get(colId);
|
|
509
|
+
if (column) {
|
|
510
|
+
column.width = Math.floor(width);
|
|
511
|
+
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
public setColumnSort(col: string | Column, sort: SortDirection, multiSort?: boolean): void {
|
|
516
|
+
const colId = typeof col === 'string' ? col : col.colId;
|
|
517
|
+
const column = this.columns.get(colId);
|
|
518
|
+
if (column) {
|
|
519
|
+
column.sort = sort;
|
|
520
|
+
if (!multiSort) {
|
|
521
|
+
this.columns.forEach((c) => {
|
|
522
|
+
if (c.colId !== colId) c.sort = null;
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
this.applySorting();
|
|
526
|
+
this.gridStateChanged$.next({ type: 'sortChanged' });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private normalizePinned(
|
|
531
|
+
pinned: boolean | 'left' | 'right' | null | undefined
|
|
532
|
+
): 'left' | 'right' | false {
|
|
533
|
+
if (pinned === 'left' || pinned === true) return 'left';
|
|
534
|
+
if (pinned === 'right') return 'right';
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
|
|
168
538
|
private getRowId(data: TData, index: number): string {
|
|
169
539
|
// 1. Try custom callback from gridOptions
|
|
170
540
|
if (this.gridOptions?.getRowId) {
|
|
@@ -175,15 +545,17 @@ export class GridService<TData = any> {
|
|
|
175
545
|
const anyData = data as any;
|
|
176
546
|
return anyData?.id?.toString() || anyData?.Id?.toString() || `row-${index}`;
|
|
177
547
|
}
|
|
178
|
-
|
|
548
|
+
|
|
179
549
|
private createGridApi(): GridApi<TData> {
|
|
180
|
-
|
|
550
|
+
const api: any = {};
|
|
551
|
+
|
|
552
|
+
Object.assign(api, {
|
|
181
553
|
// Column API
|
|
182
554
|
getColumnDefs: () => this.columnDefs,
|
|
183
555
|
setColumnDefs: (colDefs) => {
|
|
184
556
|
this.columnDefs = colDefs;
|
|
185
557
|
this.groupingDirty = true;
|
|
186
|
-
this.initializeColumns();
|
|
558
|
+
this.initializeColumns(true);
|
|
187
559
|
},
|
|
188
560
|
getColumn: (key) => {
|
|
189
561
|
const colId = typeof key === 'string' ? key : key.colId;
|
|
@@ -193,7 +565,13 @@ export class GridService<TData = any> {
|
|
|
193
565
|
getDisplayedRowAtIndex: (index) => {
|
|
194
566
|
return this.displayedRowNodes[index] || null;
|
|
195
567
|
},
|
|
196
|
-
|
|
568
|
+
getHeaderRows: () => this.getHeaderRows(),
|
|
569
|
+
getHeaderDepth: () => this.headerDepth,
|
|
570
|
+
getHeaderHeight: () => {
|
|
571
|
+
const headerHeight = this.gridOptions?.headerHeight || this.gridOptions?.rowHeight || 32;
|
|
572
|
+
return this.headerDepth * headerHeight;
|
|
573
|
+
},
|
|
574
|
+
|
|
197
575
|
// Row Data API
|
|
198
576
|
getRowData: () => [...this.filteredRowData],
|
|
199
577
|
setRowData: (rowData) => {
|
|
@@ -207,25 +585,25 @@ export class GridService<TData = any> {
|
|
|
207
585
|
getDisplayedRowCount: () => this.displayedRowNodes.length,
|
|
208
586
|
getAggregations: () => this.calculateColumnAggregations(this.filteredRowData),
|
|
209
587
|
getRowNode: (id) => this.rowNodes.get(id) || null,
|
|
210
|
-
|
|
588
|
+
|
|
211
589
|
// Selection API
|
|
212
|
-
getSelectedRows: () => Array.from(this.rowNodes.values()).filter(n => n.selected),
|
|
213
|
-
getSelectedNodes: () => Array.from(this.rowNodes.values()).filter(n => n.selected),
|
|
590
|
+
getSelectedRows: () => Array.from(this.rowNodes.values()).filter((n) => n.selected),
|
|
591
|
+
getSelectedNodes: () => Array.from(this.rowNodes.values()).filter((n) => n.selected),
|
|
214
592
|
selectAll: () => {
|
|
215
|
-
this.rowNodes.forEach(node => {
|
|
593
|
+
this.rowNodes.forEach((node) => {
|
|
216
594
|
node.selected = true;
|
|
217
595
|
this.selectedRows.add(node.id!);
|
|
218
596
|
});
|
|
219
597
|
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
220
598
|
},
|
|
221
599
|
deselectAll: () => {
|
|
222
|
-
this.rowNodes.forEach(node => {
|
|
600
|
+
this.rowNodes.forEach((node) => {
|
|
223
601
|
node.selected = false;
|
|
224
602
|
});
|
|
225
603
|
this.selectedRows.clear();
|
|
226
604
|
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
227
605
|
},
|
|
228
|
-
|
|
606
|
+
|
|
229
607
|
// Filter API
|
|
230
608
|
setFilterModel: (model) => {
|
|
231
609
|
this.filterModel = model;
|
|
@@ -238,7 +616,7 @@ export class GridService<TData = any> {
|
|
|
238
616
|
this.gridStateChanged$.next({ type: 'filterChanged' });
|
|
239
617
|
},
|
|
240
618
|
isFilterPresent: () => Object.keys(this.filterModel).length > 0,
|
|
241
|
-
|
|
619
|
+
|
|
242
620
|
// Sort API
|
|
243
621
|
setSortModel: (model) => {
|
|
244
622
|
this.sortModel = model;
|
|
@@ -247,12 +625,8 @@ export class GridService<TData = any> {
|
|
|
247
625
|
this.gridStateChanged$.next({ type: 'sortChanged' });
|
|
248
626
|
},
|
|
249
627
|
getSortModel: () => [...this.sortModel],
|
|
250
|
-
onSortChanged
|
|
251
|
-
|
|
252
|
-
this.applyFiltering(); // Re-filter and re-group after sort
|
|
253
|
-
this.gridStateChanged$.next({ type: 'sortChanged' });
|
|
254
|
-
},
|
|
255
|
-
|
|
628
|
+
// onSortChanged handled below
|
|
629
|
+
|
|
256
630
|
// Pagination API
|
|
257
631
|
paginationGetPageSize: () => 100,
|
|
258
632
|
paginationSetPageSize: () => {},
|
|
@@ -262,57 +636,92 @@ export class GridService<TData = any> {
|
|
|
262
636
|
paginationGoToLastPage: () => {},
|
|
263
637
|
paginationGoToNextPage: () => {},
|
|
264
638
|
paginationGoToPreviousPage: () => {},
|
|
265
|
-
|
|
639
|
+
|
|
266
640
|
// Export API
|
|
267
|
-
exportDataAsCsv: (params) => this.exportAsCsv(params),
|
|
641
|
+
exportDataAsCsv: (params) => this.exportAsCsv(params, api),
|
|
268
642
|
exportDataAsExcel: (params) => this.exportAsExcel(params),
|
|
269
|
-
|
|
643
|
+
downloadFile: (content, fileName, mimeType) => this.downloadFile(content, fileName, mimeType),
|
|
644
|
+
|
|
270
645
|
// Clipboard API
|
|
271
|
-
copyToClipboard: () => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
646
|
+
copyToClipboard: () => {
|
|
647
|
+
try {
|
|
648
|
+
const selectedData = api.getSelectedRows().map((node) => node.data);
|
|
649
|
+
const csv = selectedData.map((row) => Object.values(row as any).join(',')).join('\n');
|
|
650
|
+
if (typeof navigator !== 'undefined' && (navigator as any).clipboard) {
|
|
651
|
+
(navigator as any).clipboard.writeText(csv);
|
|
652
|
+
}
|
|
653
|
+
} catch (_e) {
|
|
654
|
+
// Ignore clipboard errors
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
cutToClipboard: () => {
|
|
658
|
+
api.copyToClipboard();
|
|
659
|
+
},
|
|
660
|
+
pasteFromClipboard: () => {
|
|
661
|
+
try {
|
|
662
|
+
if (typeof navigator !== 'undefined' && (navigator as any).clipboard) {
|
|
663
|
+
(navigator as any).clipboard.readText();
|
|
664
|
+
}
|
|
665
|
+
} catch (_e) {
|
|
666
|
+
// Ignore clipboard errors
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
|
|
275
670
|
// Grid State API
|
|
276
671
|
getState: () => this.getGridState(),
|
|
277
672
|
applyState: (state) => this.applyGridState(state),
|
|
278
|
-
|
|
673
|
+
|
|
674
|
+
// State Persistence API
|
|
675
|
+
saveState: (key?: string) => this.saveState(key),
|
|
676
|
+
restoreState: (key?: string) => this.restoreState(key),
|
|
677
|
+
clearState: (key?: string) => this.clearState(key),
|
|
678
|
+
hasState: (key?: string) => this.hasState(key),
|
|
679
|
+
setState: (state: GridState) => this.setState(state),
|
|
680
|
+
getUniqueValues: (field: string) => this.getUniqueValues(field),
|
|
681
|
+
|
|
279
682
|
// Focus API
|
|
280
683
|
setFocusedCell: () => {},
|
|
281
684
|
getFocusedCell: () => null,
|
|
282
|
-
|
|
685
|
+
|
|
283
686
|
// Refresh API
|
|
284
687
|
refreshCells: () => {},
|
|
285
688
|
refreshRows: (params) => {
|
|
286
689
|
if (params?.rowNodes) {
|
|
287
|
-
params.rowNodes.forEach(
|
|
690
|
+
params.rowNodes.forEach((_node) => {
|
|
288
691
|
// Trigger cell refresh
|
|
289
692
|
});
|
|
290
693
|
}
|
|
291
694
|
},
|
|
292
695
|
refreshHeader: () => {},
|
|
293
|
-
|
|
696
|
+
|
|
294
697
|
// Scroll API
|
|
295
698
|
ensureIndexVisible: () => {},
|
|
296
699
|
ensureColumnVisible: () => {},
|
|
297
|
-
|
|
700
|
+
|
|
298
701
|
// Destroy API
|
|
299
702
|
destroy: () => {
|
|
300
703
|
this.columns.clear();
|
|
301
704
|
this.rowNodes.clear();
|
|
302
705
|
this.rowData = [];
|
|
303
706
|
},
|
|
304
|
-
|
|
707
|
+
|
|
305
708
|
// Grid Information
|
|
306
709
|
getGridId: () => this.gridId,
|
|
307
|
-
getGridOption: (key) => this.gridOptions ? this.gridOptions[key] : undefined as any,
|
|
710
|
+
getGridOption: (key) => (this.gridOptions ? this.gridOptions[key] : (undefined as any)),
|
|
308
711
|
setGridOption: (key, value) => {
|
|
309
712
|
if (!this.gridOptions) {
|
|
310
713
|
this.gridOptions = {} as GridOptions<TData>;
|
|
311
714
|
}
|
|
715
|
+
if (this.gridOptions[key] === value) return;
|
|
312
716
|
this.gridOptions[key] = value;
|
|
717
|
+
|
|
718
|
+
if (key === 'rowHeight' || key === 'detailRowHeight') {
|
|
719
|
+
this.updateRowHeightCache();
|
|
720
|
+
}
|
|
721
|
+
|
|
313
722
|
this.gridStateChanged$.next({ type: 'optionChanged', key: key as string, value });
|
|
314
723
|
},
|
|
315
|
-
|
|
724
|
+
|
|
316
725
|
// Group Expansion
|
|
317
726
|
setRowNodeExpanded: (node, expanded) => {
|
|
318
727
|
if (node.id && (node.group || node.master)) {
|
|
@@ -321,13 +730,13 @@ export class GridService<TData = any> {
|
|
|
321
730
|
} else {
|
|
322
731
|
this.expandedGroups.delete(node.id);
|
|
323
732
|
}
|
|
324
|
-
|
|
733
|
+
|
|
325
734
|
if (node.group) {
|
|
326
735
|
this.applyGrouping();
|
|
327
736
|
} else {
|
|
328
737
|
this.initializeRowNodesFromFilteredData();
|
|
329
738
|
}
|
|
330
|
-
|
|
739
|
+
|
|
331
740
|
this.gridStateChanged$.next({ type: 'groupExpanded', value: expanded });
|
|
332
741
|
}
|
|
333
742
|
},
|
|
@@ -349,9 +758,9 @@ export class GridService<TData = any> {
|
|
|
349
758
|
}
|
|
350
759
|
},
|
|
351
760
|
isPivotMode: () => this.isPivotMode,
|
|
352
|
-
|
|
761
|
+
|
|
353
762
|
// Range Selection API
|
|
354
|
-
getCellRanges: () => this.cellRanges.length > 0 ? [...this.cellRanges] : null,
|
|
763
|
+
getCellRanges: () => (this.cellRanges.length > 0 ? [...this.cellRanges] : null),
|
|
355
764
|
addCellRange: (range) => {
|
|
356
765
|
this.cellRanges = [range]; // For now only support single range
|
|
357
766
|
this.gridStateChanged$.next({ type: 'rangeSelectionChanged' });
|
|
@@ -361,38 +770,321 @@ export class GridService<TData = any> {
|
|
|
361
770
|
this.cellRanges = [];
|
|
362
771
|
this.gridStateChanged$.next({ type: 'rangeSelectionChanged' });
|
|
363
772
|
}
|
|
364
|
-
}
|
|
365
|
-
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
// Column Operations
|
|
776
|
+
autoSizeColumns: (colKeys) => {
|
|
777
|
+
// Basic implementation - set reasonable widths
|
|
778
|
+
colKeys.forEach((key) => {
|
|
779
|
+
const col = typeof key === 'string' ? this.columns.get(key) : key;
|
|
780
|
+
if (col) {
|
|
781
|
+
col.width = 150;
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
},
|
|
785
|
+
getColumnState: () => {
|
|
786
|
+
return Array.from(this.columns.values()).map((col) => ({
|
|
787
|
+
colId: col.colId,
|
|
788
|
+
width: col.width,
|
|
789
|
+
hide: !col.visible,
|
|
790
|
+
pinned: col.pinned,
|
|
791
|
+
sort: col.sort,
|
|
792
|
+
sortIndex: col.sortIndex,
|
|
793
|
+
}));
|
|
794
|
+
},
|
|
795
|
+
applyColumnState: (state) => {
|
|
796
|
+
if (Array.isArray(state)) {
|
|
797
|
+
state.forEach((colState) => {
|
|
798
|
+
const col = this.columns.get(colState.colId);
|
|
799
|
+
if (col) {
|
|
800
|
+
col.width = colState.width;
|
|
801
|
+
col.visible = !colState.hide;
|
|
802
|
+
col.pinned = colState.pinned;
|
|
803
|
+
col.sort = colState.sort;
|
|
804
|
+
col.sortIndex = colState.sortIndex;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
resetColumnState: () => {
|
|
810
|
+
// Reset to initial state
|
|
811
|
+
if (this.columnDefs) {
|
|
812
|
+
this.initializeColumns();
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
// Cell Editing
|
|
817
|
+
startEditingCell: (_params) => {
|
|
818
|
+
// Basic implementation
|
|
819
|
+
},
|
|
820
|
+
stopEditing: () => {
|
|
821
|
+
// Basic implementation
|
|
822
|
+
},
|
|
823
|
+
getEditingCells: () => {
|
|
824
|
+
return [];
|
|
825
|
+
},
|
|
826
|
+
flashCells: (_params) => {
|
|
827
|
+
// Basic implementation
|
|
828
|
+
},
|
|
829
|
+
|
|
830
|
+
// Row Operations
|
|
831
|
+
resetRowHeights: () => {
|
|
832
|
+
this.updateRowHeightCache();
|
|
833
|
+
},
|
|
834
|
+
getRowHeightForRow: (rowIndex) => {
|
|
835
|
+
const node = this.displayedRowNodes[rowIndex];
|
|
836
|
+
return node?.rowHeight || this.gridOptions?.rowHeight || 32;
|
|
837
|
+
},
|
|
838
|
+
|
|
839
|
+
// Scroll Operations
|
|
840
|
+
setScrollPosition: (_params) => {
|
|
841
|
+
// Basic implementation
|
|
842
|
+
},
|
|
843
|
+
getScrollPosition: () => {
|
|
844
|
+
return { top: 0, left: 0 };
|
|
845
|
+
},
|
|
846
|
+
sizeColumnsToFit: (width) => {
|
|
847
|
+
const cols = Array.from(this.columns.values()).filter((c) => c.visible);
|
|
848
|
+
if (cols.length > 0) {
|
|
849
|
+
const colWidth = Math.floor(width / cols.length);
|
|
850
|
+
cols.forEach((col) => (col.width = colWidth));
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
|
|
854
|
+
// Pivot Mode
|
|
855
|
+
getPivotColumns: () => {
|
|
856
|
+
return [];
|
|
857
|
+
},
|
|
858
|
+
getValueColumns: () => {
|
|
859
|
+
return [];
|
|
860
|
+
},
|
|
861
|
+
toggleColumnGroup: (groupId, expanded) => this.toggleColumnGroup(groupId, expanded),
|
|
862
|
+
addRowGroupColumn: (colId) => this.addRowGroupColumn(colId),
|
|
863
|
+
removeRowGroupColumn: (colId) => this.removeRowGroupColumn(colId),
|
|
864
|
+
setRowGroupColumns: (colIds) => this.setRowGroupColumns(colIds),
|
|
865
|
+
getRowGroupColumns: () => this.getGroupColumns(),
|
|
866
|
+
getGroupDisplayType: () => {
|
|
867
|
+
return this.gridOptions?.groupDisplayType || 'singleColumn';
|
|
868
|
+
},
|
|
869
|
+
setColumnPinned: (col, pinned) => this.setColumnPinned(col, pinned),
|
|
870
|
+
moveColumn: (col, toIndex) => this.moveColumn(col, toIndex),
|
|
871
|
+
setColumnVisible: (col, visible) => this.setColumnVisible(col, visible),
|
|
872
|
+
setColumnWidth: (col, width) => this.setColumnWidth(col, width),
|
|
873
|
+
setColumnSort: (col, sort, multiSort) => this.setColumnSort(col, sort, multiSort),
|
|
874
|
+
onSortChanged: () => this.applySorting(),
|
|
875
|
+
|
|
876
|
+
// Tool Panels
|
|
877
|
+
setSideBarVisible: (_visible) => {
|
|
878
|
+
// Basic implementation
|
|
879
|
+
},
|
|
880
|
+
openToolPanel: (_panelId) => {
|
|
881
|
+
// Basic implementation
|
|
882
|
+
},
|
|
883
|
+
closeToolPanel: () => {
|
|
884
|
+
// Basic implementation
|
|
885
|
+
},
|
|
886
|
+
enableFilterToolPanel: () => {
|
|
887
|
+
// Basic implementation
|
|
888
|
+
},
|
|
889
|
+
enableColumnsToolPanel: () => {
|
|
890
|
+
// Basic implementation
|
|
891
|
+
},
|
|
892
|
+
getToolPanel: (_panelId) => {
|
|
893
|
+
return null;
|
|
894
|
+
},
|
|
895
|
+
isToolPanelShowing: () => {
|
|
896
|
+
return false;
|
|
897
|
+
},
|
|
898
|
+
|
|
899
|
+
// Context Menu
|
|
900
|
+
getContextMenuItems: () => {
|
|
901
|
+
return [];
|
|
902
|
+
},
|
|
903
|
+
getMainMenuItems: () => {
|
|
904
|
+
return [];
|
|
905
|
+
},
|
|
906
|
+
getHeaderContextMenuItems: () => {
|
|
907
|
+
return [];
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
// Event Handling
|
|
911
|
+
addEventListener: (_eventType, _listener) => {
|
|
912
|
+
// Basic implementation
|
|
913
|
+
},
|
|
914
|
+
removeEventListener: (_eventType, _listener) => {
|
|
915
|
+
// Basic implementation
|
|
916
|
+
},
|
|
917
|
+
dispatchEvent: (_event) => {
|
|
918
|
+
// Basic implementation
|
|
919
|
+
},
|
|
920
|
+
getEventPath: () => {
|
|
921
|
+
return [];
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
// Rendering
|
|
925
|
+
getRenderedNodes: () => {
|
|
926
|
+
return [...this.displayedRowNodes];
|
|
927
|
+
},
|
|
928
|
+
getFirstRenderedRow: () => {
|
|
929
|
+
return 0;
|
|
930
|
+
},
|
|
931
|
+
getLastRenderedRow: () => {
|
|
932
|
+
return this.displayedRowNodes.length - 1;
|
|
933
|
+
},
|
|
934
|
+
getVerticalPixelRange: () => {
|
|
935
|
+
return { start: 0, end: this.getTotalHeight() };
|
|
936
|
+
},
|
|
937
|
+
getHorizontalPixelRange: () => {
|
|
938
|
+
const cols = Array.from(this.columns.values()).filter((c) => c.visible);
|
|
939
|
+
const width = cols.reduce((sum, col) => sum + col.width, 0);
|
|
940
|
+
return { start: 0, end: width };
|
|
941
|
+
},
|
|
942
|
+
getPinnedWidth: () => {
|
|
943
|
+
const leftPinned = Array.from(this.columns.values()).filter((c) => c.pinned === 'left');
|
|
944
|
+
return leftPinned.reduce((sum, col) => sum + col.width, 0);
|
|
945
|
+
},
|
|
946
|
+
getRightPinnedWidth: () => {
|
|
947
|
+
const rightPinned = Array.from(this.columns.values()).filter((c) => c.pinned === 'right');
|
|
948
|
+
return rightPinned.reduce((sum, col) => sum + col.width, 0);
|
|
949
|
+
},
|
|
950
|
+
getHScrollPosition: () => {
|
|
951
|
+
return 0;
|
|
952
|
+
},
|
|
953
|
+
getVScrollPosition: () => {
|
|
954
|
+
return 0;
|
|
955
|
+
},
|
|
956
|
+
|
|
957
|
+
// Localization
|
|
958
|
+
getLocaleText: () => {
|
|
959
|
+
return '';
|
|
960
|
+
},
|
|
961
|
+
setLocaleText: (_locale, _texts) => {
|
|
962
|
+
// Basic implementation
|
|
963
|
+
},
|
|
964
|
+
|
|
965
|
+
// Charts
|
|
966
|
+
getChartModels: () => {
|
|
967
|
+
return [];
|
|
968
|
+
},
|
|
969
|
+
getChartToolbarItems: () => {
|
|
970
|
+
return [];
|
|
971
|
+
},
|
|
972
|
+
hidePopup: () => {
|
|
973
|
+
// Basic implementation
|
|
974
|
+
},
|
|
975
|
+
getSparklineOptions: () => {
|
|
976
|
+
return [];
|
|
977
|
+
},
|
|
978
|
+
|
|
979
|
+
// Grid State
|
|
980
|
+
getGridPanel: () => {
|
|
981
|
+
return null;
|
|
982
|
+
},
|
|
983
|
+
getRowContainerElement: () => {
|
|
984
|
+
return null;
|
|
985
|
+
},
|
|
986
|
+
getBodyElement: () => {
|
|
987
|
+
return null;
|
|
988
|
+
},
|
|
989
|
+
getHeaderElements: () => {
|
|
990
|
+
return [];
|
|
991
|
+
},
|
|
992
|
+
getCenterElements: () => {
|
|
993
|
+
return [];
|
|
994
|
+
},
|
|
995
|
+
getLeftElements: () => {
|
|
996
|
+
return [];
|
|
997
|
+
},
|
|
998
|
+
getRightElements: () => {
|
|
999
|
+
return [];
|
|
1000
|
+
},
|
|
1001
|
+
|
|
1002
|
+
// Disabled State
|
|
1003
|
+
setDisabled: (_disabled) => {
|
|
1004
|
+
// Basic implementation
|
|
1005
|
+
},
|
|
1006
|
+
isDisabled: () => {
|
|
1007
|
+
return false;
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
// Row Information
|
|
1011
|
+
getRowPosition: (rowIndex) => {
|
|
1012
|
+
return this.getRowY(rowIndex);
|
|
1013
|
+
},
|
|
1014
|
+
getRowStyle: (_rowIndex) => {
|
|
1015
|
+
return null;
|
|
1016
|
+
},
|
|
1017
|
+
getRowClass: (_rowIndex) => {
|
|
1018
|
+
return null;
|
|
1019
|
+
},
|
|
1020
|
+
getRowId: (node) => {
|
|
1021
|
+
return node.id;
|
|
1022
|
+
},
|
|
1023
|
+
isRowMaster: (node) => {
|
|
1024
|
+
return !!node.master;
|
|
1025
|
+
},
|
|
1026
|
+
getColumnGroups: () => {
|
|
1027
|
+
return [];
|
|
1028
|
+
},
|
|
1029
|
+
getColumnGroup: () => {
|
|
1030
|
+
return null;
|
|
1031
|
+
},
|
|
1032
|
+
|
|
1033
|
+
// Aggregation
|
|
1034
|
+
refreshAggregatedCols: () => {
|
|
1035
|
+
// Basic implementation
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
// ForEach Operations
|
|
1039
|
+
forEachNode: (callback) => {
|
|
1040
|
+
this.rowNodes.forEach((node) => callback(node));
|
|
1041
|
+
},
|
|
1042
|
+
forEachNodeAfterFilter: (callback) => {
|
|
1043
|
+
this.displayedRowNodes.forEach((node) => callback(node));
|
|
1044
|
+
},
|
|
1045
|
+
forEachNodeAfterFilterAndSort: (callback) => {
|
|
1046
|
+
this.displayedRowNodes.forEach((node) => callback(node));
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
return api;
|
|
366
1051
|
}
|
|
367
|
-
|
|
368
|
-
private applyTransaction(
|
|
1052
|
+
|
|
1053
|
+
private applyTransaction(
|
|
1054
|
+
transaction: RowDataTransaction<TData>
|
|
1055
|
+
): RowDataTransactionResult | null {
|
|
369
1056
|
const result: RowDataTransactionResult = {
|
|
370
1057
|
add: [],
|
|
371
1058
|
update: [],
|
|
372
|
-
remove: []
|
|
1059
|
+
remove: [],
|
|
373
1060
|
};
|
|
374
|
-
|
|
1061
|
+
|
|
375
1062
|
let dataChanged = false;
|
|
1063
|
+
// Track changed row indices for efficient rendering
|
|
1064
|
+
const changedRowIndices: number[] = [];
|
|
376
1065
|
|
|
377
1066
|
if (transaction.add) {
|
|
1067
|
+
const startIndex = this.rowData.length;
|
|
378
1068
|
transaction.add.forEach((data, index) => {
|
|
379
|
-
const
|
|
1069
|
+
const _id = this.getRowId(data, this.rowData.length + index);
|
|
380
1070
|
this.rowData.push(data);
|
|
381
1071
|
dataChanged = true;
|
|
382
|
-
|
|
1072
|
+
changedRowIndices.push(startIndex + index);
|
|
1073
|
+
|
|
383
1074
|
// We'll create the actual node during the pipeline re-run
|
|
384
1075
|
// but we can return a placeholder result for now as AG Grid does
|
|
385
1076
|
});
|
|
386
1077
|
}
|
|
387
|
-
|
|
1078
|
+
|
|
388
1079
|
if (transaction.update) {
|
|
389
|
-
transaction.update.forEach(data => {
|
|
1080
|
+
transaction.update.forEach((data) => {
|
|
390
1081
|
const id = this.getRowId(data, 0);
|
|
391
|
-
const index = this.rowData.findIndex(r => this.getRowId(r, 0) === id);
|
|
1082
|
+
const index = this.rowData.findIndex((r) => this.getRowId(r, 0) === id);
|
|
392
1083
|
if (index !== -1) {
|
|
393
1084
|
this.rowData[index] = data;
|
|
394
1085
|
dataChanged = true;
|
|
395
|
-
|
|
1086
|
+
changedRowIndices.push(index);
|
|
1087
|
+
|
|
396
1088
|
const existingNode = this.rowNodes.get(id);
|
|
397
1089
|
if (existingNode) {
|
|
398
1090
|
existingNode.data = data;
|
|
@@ -403,16 +1095,18 @@ export class GridService<TData = any> {
|
|
|
403
1095
|
}
|
|
404
1096
|
|
|
405
1097
|
if (transaction.remove) {
|
|
406
|
-
transaction.remove.forEach(data => {
|
|
1098
|
+
transaction.remove.forEach((data) => {
|
|
407
1099
|
const anyData = data as any;
|
|
408
|
-
const
|
|
1100
|
+
const _dataId = anyData?.id;
|
|
409
1101
|
const id = this.getRowId(data, 0);
|
|
410
|
-
|
|
411
|
-
const index = this.rowData.findIndex(r => this.getRowId(r, 0) === id);
|
|
1102
|
+
|
|
1103
|
+
const index = this.rowData.findIndex((r) => this.getRowId(r, 0) === id);
|
|
412
1104
|
if (index !== -1) {
|
|
413
|
-
const
|
|
1105
|
+
const _removedData = this.rowData.splice(index, 1)[0];
|
|
414
1106
|
dataChanged = true;
|
|
415
|
-
|
|
1107
|
+
// For removes, we mark the row as changed but it's being deleted
|
|
1108
|
+
// The component will handle this appropriately
|
|
1109
|
+
|
|
416
1110
|
const node = this.rowNodes.get(id);
|
|
417
1111
|
if (node) {
|
|
418
1112
|
this.rowNodes.delete(id);
|
|
@@ -426,22 +1120,26 @@ export class GridService<TData = any> {
|
|
|
426
1120
|
this.groupingDirty = true;
|
|
427
1121
|
this.applySorting();
|
|
428
1122
|
this.applyFiltering();
|
|
429
|
-
|
|
1123
|
+
|
|
430
1124
|
// Populate result.add after pipeline has run so we have the nodes
|
|
431
1125
|
if (transaction.add) {
|
|
432
|
-
transaction.add.forEach(data => {
|
|
1126
|
+
transaction.add.forEach((data) => {
|
|
433
1127
|
const id = this.getRowId(data, 0);
|
|
434
1128
|
const node = this.rowNodes.get(id);
|
|
435
1129
|
if (node) result.add.push(node);
|
|
436
1130
|
});
|
|
437
1131
|
}
|
|
438
1132
|
|
|
439
|
-
|
|
1133
|
+
// Emit transactionApplied with changed row indices for efficient rendering
|
|
1134
|
+
this.gridStateChanged$.next({
|
|
1135
|
+
type: 'transactionApplied',
|
|
1136
|
+
changedRowIndices,
|
|
1137
|
+
});
|
|
440
1138
|
}
|
|
441
1139
|
|
|
442
1140
|
return result;
|
|
443
1141
|
}
|
|
444
|
-
|
|
1142
|
+
|
|
445
1143
|
private applySorting(): void {
|
|
446
1144
|
this.groupingDirty = true;
|
|
447
1145
|
if (this.sortModel.length === 0) {
|
|
@@ -479,8 +1177,8 @@ export class GridService<TData = any> {
|
|
|
479
1177
|
this.filteredRowData = [...this.rowData];
|
|
480
1178
|
} else {
|
|
481
1179
|
// Apply filters with AND logic
|
|
482
|
-
this.filteredRowData = this.rowData.filter(row => {
|
|
483
|
-
return Object.keys(this.filterModel).every(colId => {
|
|
1180
|
+
this.filteredRowData = this.rowData.filter((row) => {
|
|
1181
|
+
return Object.keys(this.filterModel).every((colId) => {
|
|
484
1182
|
const filterItem = this.filterModel[colId];
|
|
485
1183
|
if (!filterItem) return true;
|
|
486
1184
|
|
|
@@ -502,7 +1200,7 @@ export class GridService<TData = any> {
|
|
|
502
1200
|
this.cumulativeRowHeights = [];
|
|
503
1201
|
let currentTotal = 0;
|
|
504
1202
|
|
|
505
|
-
this.displayedRowNodes.forEach(node => {
|
|
1203
|
+
this.displayedRowNodes.forEach((node) => {
|
|
506
1204
|
this.cumulativeRowHeights.push(currentTotal);
|
|
507
1205
|
const height = node.rowHeight || defaultHeight;
|
|
508
1206
|
currentTotal += height;
|
|
@@ -511,24 +1209,30 @@ export class GridService<TData = any> {
|
|
|
511
1209
|
this.totalHeight = currentTotal;
|
|
512
1210
|
}
|
|
513
1211
|
|
|
514
|
-
|
|
515
|
-
|
|
1212
|
+
/**
|
|
1213
|
+
* Get the Y position for a row index
|
|
1214
|
+
*/
|
|
1215
|
+
getRowY(index: number): number {
|
|
1216
|
+
if (index <= 0) return 0;
|
|
1217
|
+
if (index >= this.cumulativeRowHeights.length) return this.totalHeight;
|
|
516
1218
|
return this.cumulativeRowHeights[index];
|
|
517
1219
|
}
|
|
518
1220
|
|
|
519
1221
|
private getRowAtY(y: number): number {
|
|
520
1222
|
if (this.cumulativeRowHeights.length === 0) return 0;
|
|
521
|
-
|
|
1223
|
+
|
|
522
1224
|
// Binary search for the row at position y
|
|
523
1225
|
let low = 0;
|
|
524
1226
|
let high = this.cumulativeRowHeights.length - 1;
|
|
525
|
-
|
|
1227
|
+
|
|
526
1228
|
while (low <= high) {
|
|
527
1229
|
const mid = Math.floor((low + high) / 2);
|
|
528
1230
|
const rowY = this.cumulativeRowHeights[mid];
|
|
529
|
-
const nextRowY =
|
|
530
|
-
|
|
531
|
-
|
|
1231
|
+
const nextRowY =
|
|
1232
|
+
mid < this.cumulativeRowHeights.length - 1
|
|
1233
|
+
? this.cumulativeRowHeights[mid + 1]
|
|
1234
|
+
: this.totalHeight;
|
|
1235
|
+
|
|
532
1236
|
if (y >= rowY && y < nextRowY) {
|
|
533
1237
|
return mid;
|
|
534
1238
|
} else if (y < rowY) {
|
|
@@ -537,7 +1241,7 @@ export class GridService<TData = any> {
|
|
|
537
1241
|
low = mid + 1;
|
|
538
1242
|
}
|
|
539
1243
|
}
|
|
540
|
-
|
|
1244
|
+
|
|
541
1245
|
return low >= this.cumulativeRowHeights.length ? this.cumulativeRowHeights.length - 1 : low;
|
|
542
1246
|
}
|
|
543
1247
|
|
|
@@ -547,9 +1251,9 @@ export class GridService<TData = any> {
|
|
|
547
1251
|
|
|
548
1252
|
private getGroupColumns(): string[] {
|
|
549
1253
|
if (!this.columnDefs) return [];
|
|
550
|
-
|
|
1254
|
+
|
|
551
1255
|
const groupCols: string[] = [];
|
|
552
|
-
this.columnDefs.forEach(def => {
|
|
1256
|
+
this.columnDefs.forEach((def) => {
|
|
553
1257
|
if ('rowGroup' in def && def.rowGroup === true && def.field) {
|
|
554
1258
|
groupCols.push(def.field as string);
|
|
555
1259
|
}
|
|
@@ -559,9 +1263,9 @@ export class GridService<TData = any> {
|
|
|
559
1263
|
|
|
560
1264
|
private getPivotColumns(): string[] {
|
|
561
1265
|
if (!this.columnDefs) return [];
|
|
562
|
-
|
|
1266
|
+
|
|
563
1267
|
const pivotCols: string[] = [];
|
|
564
|
-
this.columnDefs.forEach(def => {
|
|
1268
|
+
this.columnDefs.forEach((def) => {
|
|
565
1269
|
if ('pivot' in def && def.pivot === true && def.field) {
|
|
566
1270
|
pivotCols.push(def.field as string);
|
|
567
1271
|
}
|
|
@@ -571,9 +1275,9 @@ export class GridService<TData = any> {
|
|
|
571
1275
|
|
|
572
1276
|
private getValueColumns(): ColDef<TData>[] {
|
|
573
1277
|
if (!this.columnDefs) return [];
|
|
574
|
-
|
|
1278
|
+
|
|
575
1279
|
const valueCols: ColDef<TData>[] = [];
|
|
576
|
-
this.columnDefs.forEach(def => {
|
|
1280
|
+
this.columnDefs.forEach((def) => {
|
|
577
1281
|
if (!('children' in def) && def.aggFunc && def.field) {
|
|
578
1282
|
valueCols.push(def);
|
|
579
1283
|
}
|
|
@@ -584,7 +1288,7 @@ export class GridService<TData = any> {
|
|
|
584
1288
|
private generatePivotColumnDefs(): void {
|
|
585
1289
|
const pivotColumns = this.getPivotColumns();
|
|
586
1290
|
const valueColumns = this.getValueColumns();
|
|
587
|
-
|
|
1291
|
+
|
|
588
1292
|
if (pivotColumns.length === 0 || valueColumns.length === 0) {
|
|
589
1293
|
this.pivotColumnDefs = null;
|
|
590
1294
|
return;
|
|
@@ -592,8 +1296,8 @@ export class GridService<TData = any> {
|
|
|
592
1296
|
|
|
593
1297
|
// 1. Find all unique pivot keys
|
|
594
1298
|
const pivotKeys = new Set<string>();
|
|
595
|
-
this.filteredRowData.forEach(row => {
|
|
596
|
-
const key = pivotColumns.map(col => (row as any)[col]).join('_');
|
|
1299
|
+
this.filteredRowData.forEach((row) => {
|
|
1300
|
+
const key = pivotColumns.map((col) => (row as any)[col]).join('_');
|
|
597
1301
|
pivotKeys.add(key);
|
|
598
1302
|
});
|
|
599
1303
|
|
|
@@ -602,8 +1306,8 @@ export class GridService<TData = any> {
|
|
|
602
1306
|
// 2. Generate column groups for each pivot key
|
|
603
1307
|
const newPivotColDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
|
|
604
1308
|
|
|
605
|
-
sortedPivotKeys.forEach(pivotKey => {
|
|
606
|
-
const children: ColDef<TData>[] = valueColumns.map(valCol => {
|
|
1309
|
+
sortedPivotKeys.forEach((pivotKey) => {
|
|
1310
|
+
const children: ColDef<TData>[] = valueColumns.map((valCol) => {
|
|
607
1311
|
const valueName = valCol.headerName || String(valCol.field);
|
|
608
1312
|
return {
|
|
609
1313
|
...valCol,
|
|
@@ -612,13 +1316,13 @@ export class GridService<TData = any> {
|
|
|
612
1316
|
// We use a custom field accessor for pivoted data
|
|
613
1317
|
field: `pivotData.${pivotKey}.${String(valCol.field)}` as any,
|
|
614
1318
|
pivot: false, // These are the results, not the pivot sources
|
|
615
|
-
rowGroup: false
|
|
1319
|
+
rowGroup: false,
|
|
616
1320
|
};
|
|
617
1321
|
});
|
|
618
1322
|
|
|
619
1323
|
newPivotColDefs.push({
|
|
620
1324
|
headerName: pivotKey,
|
|
621
|
-
children: children
|
|
1325
|
+
children: children,
|
|
622
1326
|
});
|
|
623
1327
|
});
|
|
624
1328
|
|
|
@@ -627,7 +1331,7 @@ export class GridService<TData = any> {
|
|
|
627
1331
|
|
|
628
1332
|
private applyGrouping(): void {
|
|
629
1333
|
const groupColumns = this.getGroupColumns();
|
|
630
|
-
|
|
1334
|
+
|
|
631
1335
|
if (this.isPivotMode) {
|
|
632
1336
|
this.generatePivotColumnDefs();
|
|
633
1337
|
// If we have pivot columns but weren't grouping, we need to initialize them
|
|
@@ -645,28 +1349,54 @@ export class GridService<TData = any> {
|
|
|
645
1349
|
// Only re-group if filters or data changed
|
|
646
1350
|
if (this.groupingDirty || !this.cachedGroupedData) {
|
|
647
1351
|
this.cachedGroupedData = this.groupByColumns(this.filteredRowData, groupColumns, 0);
|
|
648
|
-
|
|
1352
|
+
|
|
649
1353
|
if (this.isPivotMode) {
|
|
650
|
-
// Already called above, but we do it again after grouping to be sure
|
|
1354
|
+
// Already called above, but we do it again after grouping to be sure
|
|
651
1355
|
// (though in Pivot Mode we usually want grouping)
|
|
652
1356
|
this.generatePivotColumnDefs();
|
|
653
1357
|
this.initializeColumns(); // Re-initialize with new pivot columns
|
|
654
1358
|
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
655
1359
|
}
|
|
656
|
-
|
|
1360
|
+
|
|
657
1361
|
this.groupingDirty = false;
|
|
1362
|
+
|
|
1363
|
+
// Apply default expansion only on structural/data changes
|
|
1364
|
+
const defaultExpanded = this.gridOptions?.groupDefaultExpanded ?? 0;
|
|
1365
|
+
if (defaultExpanded !== 0) {
|
|
1366
|
+
this.applyDefaultExpansion(this.cachedGroupedData, 0, defaultExpanded);
|
|
1367
|
+
}
|
|
658
1368
|
}
|
|
659
1369
|
|
|
660
|
-
// Re-initialize from cache (respects current expansion state)
|
|
661
|
-
this.
|
|
1370
|
+
// Re-initialize from cache (respects current expansion state in this.expandedGroups)
|
|
1371
|
+
this.syncExpansionStateFromSet(this.cachedGroupedData);
|
|
662
1372
|
this.initializeRowNodesFromGroupedData();
|
|
663
1373
|
}
|
|
664
1374
|
|
|
665
|
-
private
|
|
1375
|
+
private applyDefaultExpansion(
|
|
1376
|
+
groupedData: (TData | GroupRowNode<TData>)[],
|
|
1377
|
+
level: number,
|
|
1378
|
+
defaultExpanded: number
|
|
1379
|
+
): void {
|
|
1380
|
+
const isDefaultExpanded = defaultExpanded === -1 || level < defaultExpanded;
|
|
1381
|
+
if (!isDefaultExpanded) return;
|
|
1382
|
+
|
|
1383
|
+
for (const item of groupedData) {
|
|
1384
|
+
if (this.isGroupRowNode(item)) {
|
|
1385
|
+
this.expandedGroups.add(item.id);
|
|
1386
|
+
if (item.children) {
|
|
1387
|
+
this.applyDefaultExpansion(item.children, level + 1, defaultExpanded);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private syncExpansionStateFromSet(groupedData: (TData | GroupRowNode<TData>)[]): void {
|
|
666
1394
|
for (const item of groupedData) {
|
|
667
1395
|
if (this.isGroupRowNode(item)) {
|
|
668
1396
|
item.expanded = this.expandedGroups.has(item.id);
|
|
669
|
-
|
|
1397
|
+
if (item.children) {
|
|
1398
|
+
this.syncExpansionStateFromSet(item.children);
|
|
1399
|
+
}
|
|
670
1400
|
}
|
|
671
1401
|
}
|
|
672
1402
|
}
|
|
@@ -687,7 +1417,7 @@ export class GridService<TData = any> {
|
|
|
687
1417
|
children: data,
|
|
688
1418
|
expanded: true,
|
|
689
1419
|
aggregation: this.calculateAggregations(data, 'Summary'),
|
|
690
|
-
pivotData: this.calculatePivotData(data)
|
|
1420
|
+
pivotData: this.calculatePivotData(data),
|
|
691
1421
|
};
|
|
692
1422
|
return [rootGroup];
|
|
693
1423
|
}
|
|
@@ -698,19 +1428,19 @@ export class GridService<TData = any> {
|
|
|
698
1428
|
const groups = new Map<any, TData[]>();
|
|
699
1429
|
|
|
700
1430
|
// Group data by the current field
|
|
701
|
-
data.forEach(item => {
|
|
1431
|
+
data.forEach((item) => {
|
|
702
1432
|
const key = (item as any)[groupField];
|
|
703
1433
|
if (!groups.has(key)) {
|
|
704
1434
|
groups.set(key, []);
|
|
705
1435
|
}
|
|
706
|
-
groups.get(key)
|
|
1436
|
+
groups.get(key)?.push(item);
|
|
707
1437
|
});
|
|
708
1438
|
|
|
709
1439
|
// Create group nodes
|
|
710
1440
|
const result: (TData | GroupRowNode<TData>)[] = [];
|
|
711
1441
|
groups.forEach((items, key) => {
|
|
712
1442
|
const children = this.groupByColumns(items, groupColumns, level + 1);
|
|
713
|
-
|
|
1443
|
+
|
|
714
1444
|
const groupNode: GroupRowNode<TData> = {
|
|
715
1445
|
id: `group-${groupField}-${key}-${level}`,
|
|
716
1446
|
groupKey: key,
|
|
@@ -719,16 +1449,16 @@ export class GridService<TData = any> {
|
|
|
719
1449
|
children,
|
|
720
1450
|
expanded: this.expandedGroups.has(`group-${groupField}-${key}-${level}`),
|
|
721
1451
|
aggregation: this.calculateAggregations(items, groupField),
|
|
722
|
-
pivotData: this.isPivotMode ? this.calculatePivotData(items) : undefined
|
|
1452
|
+
pivotData: this.isPivotMode ? this.calculatePivotData(items) : undefined,
|
|
723
1453
|
};
|
|
724
|
-
|
|
1454
|
+
|
|
725
1455
|
result.push(groupNode);
|
|
726
1456
|
});
|
|
727
1457
|
|
|
728
1458
|
return result;
|
|
729
1459
|
}
|
|
730
1460
|
|
|
731
|
-
private calculateAggregations(data: TData[],
|
|
1461
|
+
private calculateAggregations(data: TData[], _groupField: string): { [field: string]: any } {
|
|
732
1462
|
return this.calculateColumnAggregations(data);
|
|
733
1463
|
}
|
|
734
1464
|
|
|
@@ -737,12 +1467,12 @@ export class GridService<TData = any> {
|
|
|
737
1467
|
const pivotGroups = new Map<string, TData[]>();
|
|
738
1468
|
|
|
739
1469
|
// Sub-group by pivot columns within this row group
|
|
740
|
-
data.forEach(item => {
|
|
741
|
-
const key = pivotColumns.map(col => (item as any)[col]).join('_');
|
|
1470
|
+
data.forEach((item) => {
|
|
1471
|
+
const key = pivotColumns.map((col) => (item as any)[col]).join('_');
|
|
742
1472
|
if (!pivotGroups.has(key)) {
|
|
743
1473
|
pivotGroups.set(key, []);
|
|
744
1474
|
}
|
|
745
|
-
pivotGroups.get(key)
|
|
1475
|
+
pivotGroups.get(key)?.push(item);
|
|
746
1476
|
});
|
|
747
1477
|
|
|
748
1478
|
const pivotData: { [pivotKey: string]: { [field: string]: any } } = {};
|
|
@@ -755,18 +1485,20 @@ export class GridService<TData = any> {
|
|
|
755
1485
|
|
|
756
1486
|
public calculateColumnAggregations(data: TData[]): { [field: string]: any } {
|
|
757
1487
|
const aggregations: { [field: string]: any } = {};
|
|
758
|
-
|
|
1488
|
+
|
|
759
1489
|
if (!this.columnDefs) return aggregations;
|
|
760
1490
|
|
|
761
|
-
this.columnDefs.forEach(def => {
|
|
1491
|
+
this.columnDefs.forEach((def) => {
|
|
762
1492
|
// Skip column groups
|
|
763
1493
|
if ('children' in def) return;
|
|
764
|
-
|
|
1494
|
+
|
|
765
1495
|
if (!def.field || !def.aggFunc) return;
|
|
766
|
-
|
|
1496
|
+
|
|
767
1497
|
const field = def.field as string;
|
|
768
|
-
const values = data
|
|
769
|
-
|
|
1498
|
+
const values = data
|
|
1499
|
+
.map((item) => (item as any)[field])
|
|
1500
|
+
.filter((v) => v !== null && v !== undefined);
|
|
1501
|
+
|
|
770
1502
|
if (values.length === 0) return;
|
|
771
1503
|
|
|
772
1504
|
if (typeof def.aggFunc === 'function') {
|
|
@@ -779,13 +1511,14 @@ export class GridService<TData = any> {
|
|
|
779
1511
|
aggregations[field] = values.reduce((sum, v) => sum + (Number(v) || 0), 0);
|
|
780
1512
|
break;
|
|
781
1513
|
case 'avg':
|
|
782
|
-
aggregations[field] =
|
|
1514
|
+
aggregations[field] =
|
|
1515
|
+
values.reduce((sum, v) => sum + (Number(v) || 0), 0) / values.length;
|
|
783
1516
|
break;
|
|
784
1517
|
case 'min':
|
|
785
|
-
aggregations[field] = Math.min(...values.map(v => Number(v) || 0));
|
|
1518
|
+
aggregations[field] = Math.min(...values.map((v) => Number(v) || 0));
|
|
786
1519
|
break;
|
|
787
1520
|
case 'max':
|
|
788
|
-
aggregations[field] = Math.max(...values.map(v => Number(v) || 0));
|
|
1521
|
+
aggregations[field] = Math.max(...values.map((v) => Number(v) || 0));
|
|
789
1522
|
break;
|
|
790
1523
|
case 'count':
|
|
791
1524
|
aggregations[field] = values.length;
|
|
@@ -803,7 +1536,7 @@ export class GridService<TData = any> {
|
|
|
803
1536
|
// DO NOT CLEAR this.rowNodes - reuse existing nodes to preserve state
|
|
804
1537
|
this.displayedRowNodes = [];
|
|
805
1538
|
const flattened = this.flattenGroupedDataWithLevel(this.cachedGroupedData || []);
|
|
806
|
-
|
|
1539
|
+
|
|
807
1540
|
flattened.forEach((entry, index) => {
|
|
808
1541
|
const { item, level } = entry;
|
|
809
1542
|
let id: string;
|
|
@@ -815,11 +1548,11 @@ export class GridService<TData = any> {
|
|
|
815
1548
|
// Group node
|
|
816
1549
|
id = item.id;
|
|
817
1550
|
// Re-use aggregation data from the group node
|
|
818
|
-
data = {
|
|
1551
|
+
data = {
|
|
819
1552
|
...item.aggregation,
|
|
820
1553
|
pivotData: item.pivotData,
|
|
821
1554
|
[item.groupField]: item.groupKey,
|
|
822
|
-
'ag-Grid-AutoColumn': item.groupKey
|
|
1555
|
+
'ag-Grid-AutoColumn': item.groupKey,
|
|
823
1556
|
} as TData;
|
|
824
1557
|
isGroup = true;
|
|
825
1558
|
expanded = item.expanded;
|
|
@@ -856,11 +1589,30 @@ export class GridService<TData = any> {
|
|
|
856
1589
|
firstChild: index === 0,
|
|
857
1590
|
lastChild: index === flattened.length - 1,
|
|
858
1591
|
rowIndex: index,
|
|
859
|
-
displayedRowIndex: index
|
|
1592
|
+
displayedRowIndex: index,
|
|
1593
|
+
setSelected: (selected: boolean, clearSelection: boolean = false) => {
|
|
1594
|
+
const changed = node.selected !== selected || clearSelection;
|
|
1595
|
+
if (!changed) return;
|
|
1596
|
+
|
|
1597
|
+
if (clearSelection) {
|
|
1598
|
+
this.rowNodes.forEach((n) => {
|
|
1599
|
+
n.selected = false;
|
|
1600
|
+
});
|
|
1601
|
+
this.selectedRows.clear();
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
node.selected = selected;
|
|
1605
|
+
if (selected) {
|
|
1606
|
+
this.selectedRows.add(node.id!);
|
|
1607
|
+
} else {
|
|
1608
|
+
this.selectedRows.delete(node.id!);
|
|
1609
|
+
}
|
|
1610
|
+
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
1611
|
+
},
|
|
860
1612
|
};
|
|
861
1613
|
this.rowNodes.set(id, node);
|
|
862
1614
|
}
|
|
863
|
-
|
|
1615
|
+
|
|
864
1616
|
this.displayedRowNodes.push(node);
|
|
865
1617
|
});
|
|
866
1618
|
|
|
@@ -874,11 +1626,11 @@ export class GridService<TData = any> {
|
|
|
874
1626
|
private flattenGroupedDataWithLevel(
|
|
875
1627
|
groupedData: (TData | GroupRowNode<TData>)[],
|
|
876
1628
|
level: number = 0,
|
|
877
|
-
result: { item: TData | GroupRowNode<TData
|
|
878
|
-
): { item: TData | GroupRowNode<TData
|
|
1629
|
+
result: { item: TData | GroupRowNode<TData>; level: number }[] = []
|
|
1630
|
+
): { item: TData | GroupRowNode<TData>; level: number }[] {
|
|
879
1631
|
for (const item of groupedData) {
|
|
880
1632
|
result.push({ item, level });
|
|
881
|
-
|
|
1633
|
+
|
|
882
1634
|
if (this.isGroupRowNode(item)) {
|
|
883
1635
|
if (item.expanded) {
|
|
884
1636
|
this.flattenGroupedDataWithLevel(item.children, level + 1, result);
|
|
@@ -904,11 +1656,43 @@ export class GridService<TData = any> {
|
|
|
904
1656
|
return this.matchesDateFilter(String(value), type, filter, filterTo);
|
|
905
1657
|
case 'boolean':
|
|
906
1658
|
return this.matchesBooleanFilter(value, filter);
|
|
1659
|
+
case 'set':
|
|
1660
|
+
return this.matchesSetFilter(value, filterItem);
|
|
907
1661
|
default:
|
|
908
1662
|
return true;
|
|
909
1663
|
}
|
|
910
1664
|
}
|
|
911
1665
|
|
|
1666
|
+
private matchesSetFilter(value: any, filterItem: FilterModelItem): boolean {
|
|
1667
|
+
// Set filter: value must be in the selected values array
|
|
1668
|
+
const filterValues = filterItem.values;
|
|
1669
|
+
if (!Array.isArray(filterValues) || filterValues.length === 0) {
|
|
1670
|
+
return true; // No filter applied
|
|
1671
|
+
}
|
|
1672
|
+
return filterValues.includes(value);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Get unique values for a column (for Set Filter)
|
|
1677
|
+
*/
|
|
1678
|
+
getUniqueValues(field: string): any[] {
|
|
1679
|
+
const values = new Set<any>();
|
|
1680
|
+
|
|
1681
|
+
this.rowData.forEach((data) => {
|
|
1682
|
+
const value = (data as any)[field];
|
|
1683
|
+
if (value !== null && value !== undefined) {
|
|
1684
|
+
values.add(value);
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
return Array.from(values).sort((a, b) => {
|
|
1689
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
1690
|
+
return a.localeCompare(b);
|
|
1691
|
+
}
|
|
1692
|
+
return String(a).localeCompare(String(b));
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
912
1696
|
private matchesTextFilter(value: string, type: string | undefined, filter: any): boolean {
|
|
913
1697
|
if (!type || filter === null || filter === undefined) {
|
|
914
1698
|
return true;
|
|
@@ -935,8 +1719,13 @@ export class GridService<TData = any> {
|
|
|
935
1719
|
}
|
|
936
1720
|
}
|
|
937
1721
|
|
|
938
|
-
private matchesNumberFilter(
|
|
939
|
-
|
|
1722
|
+
private matchesNumberFilter(
|
|
1723
|
+
value: number,
|
|
1724
|
+
type: string | undefined,
|
|
1725
|
+
filter: any,
|
|
1726
|
+
filterTo?: any
|
|
1727
|
+
): boolean {
|
|
1728
|
+
if (type === undefined || filter === null || filter === undefined || Number.isNaN(value)) {
|
|
940
1729
|
return true;
|
|
941
1730
|
}
|
|
942
1731
|
|
|
@@ -955,15 +1744,21 @@ export class GridService<TData = any> {
|
|
|
955
1744
|
return value < filterNum;
|
|
956
1745
|
case 'lessThanOrEqual':
|
|
957
1746
|
return value <= filterNum;
|
|
958
|
-
case 'inRange':
|
|
1747
|
+
case 'inRange': {
|
|
959
1748
|
const filterToNum = Number(filterTo);
|
|
960
1749
|
return value >= filterNum && value <= filterToNum;
|
|
1750
|
+
}
|
|
961
1751
|
default:
|
|
962
1752
|
return true;
|
|
963
1753
|
}
|
|
964
1754
|
}
|
|
965
1755
|
|
|
966
|
-
private matchesDateFilter(
|
|
1756
|
+
private matchesDateFilter(
|
|
1757
|
+
value: string,
|
|
1758
|
+
type: string | undefined,
|
|
1759
|
+
filter: any,
|
|
1760
|
+
filterTo?: any
|
|
1761
|
+
): boolean {
|
|
967
1762
|
if (!type || !filter) {
|
|
968
1763
|
return true;
|
|
969
1764
|
}
|
|
@@ -1005,16 +1800,16 @@ export class GridService<TData = any> {
|
|
|
1005
1800
|
this.groupingDirty = true;
|
|
1006
1801
|
// DO NOT CLEAR this.rowNodes - reuse existing nodes
|
|
1007
1802
|
this.displayedRowNodes = [];
|
|
1008
|
-
|
|
1803
|
+
|
|
1009
1804
|
// Separate rows by pinned state
|
|
1010
1805
|
const pinnedTopRows: TData[] = [];
|
|
1011
1806
|
const pinnedBottomRows: TData[] = [];
|
|
1012
1807
|
const normalRows: TData[] = [];
|
|
1013
|
-
|
|
1014
|
-
this.filteredRowData.forEach(data => {
|
|
1808
|
+
|
|
1809
|
+
this.filteredRowData.forEach((data) => {
|
|
1015
1810
|
const anyData = data as any;
|
|
1016
1811
|
const pinned = anyData?.pinned;
|
|
1017
|
-
|
|
1812
|
+
|
|
1018
1813
|
if (pinned === 'top') {
|
|
1019
1814
|
pinnedTopRows.push(data);
|
|
1020
1815
|
} else if (pinned === 'bottom') {
|
|
@@ -1023,15 +1818,16 @@ export class GridService<TData = any> {
|
|
|
1023
1818
|
normalRows.push(data);
|
|
1024
1819
|
}
|
|
1025
1820
|
});
|
|
1026
|
-
|
|
1821
|
+
|
|
1027
1822
|
const orderedRows = [...pinnedTopRows, ...normalRows, ...pinnedBottomRows];
|
|
1028
1823
|
|
|
1029
1824
|
orderedRows.forEach((data, index) => {
|
|
1030
1825
|
const id = this.getRowId(data, index);
|
|
1031
1826
|
const anyData = data as any;
|
|
1032
1827
|
const rowPinned = anyData?.pinned || false;
|
|
1033
|
-
const isMaster =
|
|
1034
|
-
|
|
1828
|
+
const isMaster =
|
|
1829
|
+
this.gridOptions?.masterDetail &&
|
|
1830
|
+
(this.gridOptions.isRowMaster ? this.gridOptions.isRowMaster(data) : true);
|
|
1035
1831
|
|
|
1036
1832
|
let node = this.rowNodes.get(id);
|
|
1037
1833
|
if (node) {
|
|
@@ -1058,11 +1854,30 @@ export class GridService<TData = any> {
|
|
|
1058
1854
|
firstChild: index === 0,
|
|
1059
1855
|
lastChild: index === orderedRows.length - 1,
|
|
1060
1856
|
rowIndex: index,
|
|
1061
|
-
displayedRowIndex: this.displayedRowNodes.length
|
|
1857
|
+
displayedRowIndex: this.displayedRowNodes.length,
|
|
1858
|
+
setSelected: (selected: boolean, clearSelection: boolean = false) => {
|
|
1859
|
+
const changed = node.selected !== selected || clearSelection;
|
|
1860
|
+
if (!changed) return;
|
|
1861
|
+
|
|
1862
|
+
if (clearSelection) {
|
|
1863
|
+
this.rowNodes.forEach((n) => {
|
|
1864
|
+
n.selected = false;
|
|
1865
|
+
});
|
|
1866
|
+
this.selectedRows.clear();
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
node.selected = selected;
|
|
1870
|
+
if (selected) {
|
|
1871
|
+
this.selectedRows.add(node.id!);
|
|
1872
|
+
} else {
|
|
1873
|
+
this.selectedRows.delete(node.id!);
|
|
1874
|
+
}
|
|
1875
|
+
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
1876
|
+
},
|
|
1062
1877
|
};
|
|
1063
1878
|
this.rowNodes.set(id!, node);
|
|
1064
1879
|
}
|
|
1065
|
-
|
|
1880
|
+
|
|
1066
1881
|
this.displayedRowNodes.push(node);
|
|
1067
1882
|
|
|
1068
1883
|
// If master row is expanded, insert a detail node
|
|
@@ -1085,7 +1900,11 @@ export class GridService<TData = any> {
|
|
|
1085
1900
|
firstChild: false,
|
|
1086
1901
|
lastChild: false,
|
|
1087
1902
|
rowIndex: null,
|
|
1088
|
-
displayedRowIndex: this.displayedRowNodes.length
|
|
1903
|
+
displayedRowIndex: this.displayedRowNodes.length,
|
|
1904
|
+
setSelected: (selected: boolean) => {
|
|
1905
|
+
detailNode!.selected = selected;
|
|
1906
|
+
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
1907
|
+
},
|
|
1089
1908
|
};
|
|
1090
1909
|
this.rowNodes.set(detailId, detailNode);
|
|
1091
1910
|
} else {
|
|
@@ -1097,7 +1916,7 @@ export class GridService<TData = any> {
|
|
|
1097
1916
|
|
|
1098
1917
|
this.updateRowHeightCache();
|
|
1099
1918
|
}
|
|
1100
|
-
|
|
1919
|
+
|
|
1101
1920
|
private compareValues(a: any, b: any): number {
|
|
1102
1921
|
if (a === b) return 0;
|
|
1103
1922
|
if (a === null || a === undefined) return 1;
|
|
@@ -1107,10 +1926,10 @@ export class GridService<TData = any> {
|
|
|
1107
1926
|
}
|
|
1108
1927
|
return String(a).localeCompare(String(b));
|
|
1109
1928
|
}
|
|
1110
|
-
|
|
1929
|
+
|
|
1111
1930
|
private getGridState(): GridState {
|
|
1112
1931
|
const filterState: { [key: string]: FilterModelItem } = {};
|
|
1113
|
-
Object.keys(this.filterModel).forEach(key => {
|
|
1932
|
+
Object.keys(this.filterModel).forEach((key) => {
|
|
1114
1933
|
const item = this.filterModel[key];
|
|
1115
1934
|
if (item) {
|
|
1116
1935
|
filterState[key] = item;
|
|
@@ -1119,24 +1938,24 @@ export class GridService<TData = any> {
|
|
|
1119
1938
|
|
|
1120
1939
|
// Get pinned columns
|
|
1121
1940
|
const allColumns = Array.from(this.columns.values());
|
|
1122
|
-
const leftPinned = allColumns.filter(c => c.pinned === 'left').map(c => c.colId);
|
|
1123
|
-
const rightPinned = allColumns.filter(c => c.pinned === 'right').map(c => c.colId);
|
|
1941
|
+
const leftPinned = allColumns.filter((c) => c.pinned === 'left').map((c) => c.colId);
|
|
1942
|
+
const rightPinned = allColumns.filter((c) => c.pinned === 'right').map((c) => c.colId);
|
|
1124
1943
|
|
|
1125
1944
|
return {
|
|
1126
1945
|
sort: { sortModel: [...this.sortModel] },
|
|
1127
1946
|
filter: filterState,
|
|
1128
1947
|
columnPinning: { left: leftPinned, right: rightPinned },
|
|
1129
|
-
columnOrder: allColumns.map(col => ({
|
|
1948
|
+
columnOrder: allColumns.map((col) => ({
|
|
1130
1949
|
colId: col.colId,
|
|
1131
1950
|
width: col.width,
|
|
1132
1951
|
hide: !col.visible,
|
|
1133
1952
|
pinned: col.pinned,
|
|
1134
1953
|
sort: col.sort,
|
|
1135
|
-
sortIndex: col.sortIndex
|
|
1136
|
-
}))
|
|
1954
|
+
sortIndex: col.sortIndex,
|
|
1955
|
+
})),
|
|
1137
1956
|
};
|
|
1138
1957
|
}
|
|
1139
|
-
|
|
1958
|
+
|
|
1140
1959
|
private applyGridState(state: GridState): void {
|
|
1141
1960
|
if (state.sort) {
|
|
1142
1961
|
this.sortModel = state.sort.sortModel;
|
|
@@ -1146,7 +1965,7 @@ export class GridService<TData = any> {
|
|
|
1146
1965
|
this.filterModel = state.filter;
|
|
1147
1966
|
}
|
|
1148
1967
|
if (state.columnOrder) {
|
|
1149
|
-
state.columnOrder.forEach(colState => {
|
|
1968
|
+
state.columnOrder.forEach((colState) => {
|
|
1150
1969
|
const column = this.columns.get(colState.colId);
|
|
1151
1970
|
if (column) {
|
|
1152
1971
|
column.width = colState.width;
|
|
@@ -1158,42 +1977,42 @@ export class GridService<TData = any> {
|
|
|
1158
1977
|
});
|
|
1159
1978
|
}
|
|
1160
1979
|
}
|
|
1161
|
-
|
|
1162
|
-
private exportAsCsv(params?: CsvExportParams): void {
|
|
1980
|
+
|
|
1981
|
+
private exportAsCsv(params?: CsvExportParams, api?: any): void {
|
|
1163
1982
|
const fileName = params?.fileName || 'export.csv';
|
|
1164
1983
|
const delimiter = params?.delimiter || ',';
|
|
1165
1984
|
const skipHeader = params?.skipHeader || false;
|
|
1166
1985
|
const columnKeys = params?.columnKeys;
|
|
1167
1986
|
|
|
1168
1987
|
// Get columns to export
|
|
1169
|
-
let columnsToExport = this.getAllColumns().filter(col => col.visible);
|
|
1988
|
+
let columnsToExport = this.getAllColumns().filter((col) => col.visible);
|
|
1170
1989
|
if (columnKeys && columnKeys.length > 0) {
|
|
1171
|
-
columnsToExport = columnsToExport.filter(col => columnKeys.includes(col.colId));
|
|
1990
|
+
columnsToExport = columnsToExport.filter((col) => columnKeys.includes(col.colId));
|
|
1172
1991
|
}
|
|
1173
1992
|
|
|
1174
1993
|
// Build headers
|
|
1175
|
-
const headers = columnsToExport.map(col => {
|
|
1994
|
+
const headers = columnsToExport.map((col) => {
|
|
1176
1995
|
const headerName = col.headerName || col.colId;
|
|
1177
1996
|
// Escape quotes and wrap in quotes if contains delimiter
|
|
1178
1997
|
if (headerName.includes(delimiter) || headerName.includes('"') || headerName.includes('\n')) {
|
|
1179
|
-
return
|
|
1998
|
+
return `"${headerName.replace(/"/g, '""')}"`;
|
|
1180
1999
|
}
|
|
1181
2000
|
return headerName;
|
|
1182
2001
|
});
|
|
1183
2002
|
|
|
1184
2003
|
// Build rows
|
|
1185
|
-
const rows = this.rowData.map(data => {
|
|
1186
|
-
return columnsToExport.map(col => {
|
|
2004
|
+
const rows = this.rowData.map((data) => {
|
|
2005
|
+
return columnsToExport.map((col) => {
|
|
1187
2006
|
const value = (data as any)[col.field!];
|
|
1188
2007
|
let cellValue = '';
|
|
1189
|
-
|
|
2008
|
+
|
|
1190
2009
|
if (value !== null && value !== undefined) {
|
|
1191
2010
|
cellValue = String(value);
|
|
1192
2011
|
}
|
|
1193
|
-
|
|
2012
|
+
|
|
1194
2013
|
// Escape quotes and wrap in quotes if contains special chars
|
|
1195
2014
|
if (cellValue.includes(delimiter) || cellValue.includes('"') || cellValue.includes('\n')) {
|
|
1196
|
-
return
|
|
2015
|
+
return `"${cellValue.replace(/"/g, '""')}"`;
|
|
1197
2016
|
}
|
|
1198
2017
|
return cellValue;
|
|
1199
2018
|
});
|
|
@@ -1202,12 +2021,16 @@ export class GridService<TData = any> {
|
|
|
1202
2021
|
// Build CSV content
|
|
1203
2022
|
let csvContent = '';
|
|
1204
2023
|
if (!skipHeader) {
|
|
1205
|
-
csvContent += headers.join(delimiter)
|
|
2024
|
+
csvContent += `${headers.join(delimiter)}\n`;
|
|
1206
2025
|
}
|
|
1207
|
-
csvContent += rows.map(row => row.join(delimiter)).join('\n');
|
|
2026
|
+
csvContent += rows.map((row) => row.join(delimiter)).join('\n');
|
|
1208
2027
|
|
|
1209
2028
|
// Download CSV
|
|
1210
|
-
|
|
2029
|
+
if (api) {
|
|
2030
|
+
api.downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
|
|
2031
|
+
} else {
|
|
2032
|
+
this.downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
|
|
2033
|
+
}
|
|
1211
2034
|
}
|
|
1212
2035
|
|
|
1213
2036
|
private exportAsExcel(params?: ExcelExportParams): void {
|
|
@@ -1216,9 +2039,9 @@ export class GridService<TData = any> {
|
|
|
1216
2039
|
const skipHeader = params?.skipHeader || false;
|
|
1217
2040
|
|
|
1218
2041
|
// Get columns to export
|
|
1219
|
-
let columnsToExport = this.getAllColumns().filter(col => col.visible);
|
|
2042
|
+
let columnsToExport = this.getAllColumns().filter((col) => col.visible);
|
|
1220
2043
|
if (params?.columnKeys && params.columnKeys.length > 0) {
|
|
1221
|
-
columnsToExport = columnsToExport.filter(col => params.columnKeys
|
|
2044
|
+
columnsToExport = columnsToExport.filter((col) => params.columnKeys?.includes(col.colId));
|
|
1222
2045
|
}
|
|
1223
2046
|
|
|
1224
2047
|
const workbook = new Workbook();
|
|
@@ -1226,18 +2049,18 @@ export class GridService<TData = any> {
|
|
|
1226
2049
|
|
|
1227
2050
|
// Add headers
|
|
1228
2051
|
if (!skipHeader) {
|
|
1229
|
-
const headerRow = worksheet.addRow(columnsToExport.map(col => col.headerName || col.colId));
|
|
2052
|
+
const headerRow = worksheet.addRow(columnsToExport.map((col) => col.headerName || col.colId));
|
|
1230
2053
|
headerRow.font = { bold: true };
|
|
1231
2054
|
headerRow.fill = {
|
|
1232
2055
|
type: 'pattern',
|
|
1233
2056
|
pattern: 'solid',
|
|
1234
|
-
fgColor: { argb: 'FFF0F0F0' }
|
|
2057
|
+
fgColor: { argb: 'FFF0F0F0' },
|
|
1235
2058
|
};
|
|
1236
2059
|
}
|
|
1237
2060
|
|
|
1238
2061
|
// Add data
|
|
1239
|
-
this.rowData.forEach(data => {
|
|
1240
|
-
const rowValues = columnsToExport.map(col => {
|
|
2062
|
+
this.rowData.forEach((data) => {
|
|
2063
|
+
const rowValues = columnsToExport.map((col) => {
|
|
1241
2064
|
const value = (data as any)[col.field!];
|
|
1242
2065
|
return value !== null && value !== undefined ? value : '';
|
|
1243
2066
|
});
|
|
@@ -1245,9 +2068,9 @@ export class GridService<TData = any> {
|
|
|
1245
2068
|
});
|
|
1246
2069
|
|
|
1247
2070
|
// Auto-fit columns (basic implementation)
|
|
1248
|
-
worksheet.columns.forEach((column,
|
|
2071
|
+
worksheet.columns.forEach((column, _i) => {
|
|
1249
2072
|
let maxLength = 0;
|
|
1250
|
-
column.eachCell
|
|
2073
|
+
column.eachCell?.({ includeEmpty: true }, (cell) => {
|
|
1251
2074
|
const columnLength = cell.value ? cell.value.toString().length : 10;
|
|
1252
2075
|
if (columnLength > maxLength) {
|
|
1253
2076
|
maxLength = columnLength;
|
|
@@ -1257,8 +2080,10 @@ export class GridService<TData = any> {
|
|
|
1257
2080
|
});
|
|
1258
2081
|
|
|
1259
2082
|
// Generate buffer and download
|
|
1260
|
-
workbook.xlsx.writeBuffer().then(buffer => {
|
|
1261
|
-
const blob = new Blob([buffer], {
|
|
2083
|
+
workbook.xlsx.writeBuffer().then((buffer) => {
|
|
2084
|
+
const blob = new Blob([buffer], {
|
|
2085
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
2086
|
+
});
|
|
1262
2087
|
const url = URL.createObjectURL(blob);
|
|
1263
2088
|
const link = document.createElement('a');
|
|
1264
2089
|
link.href = url;
|
|
@@ -1267,11 +2092,11 @@ export class GridService<TData = any> {
|
|
|
1267
2092
|
URL.revokeObjectURL(url);
|
|
1268
2093
|
});
|
|
1269
2094
|
}
|
|
1270
|
-
|
|
2095
|
+
|
|
1271
2096
|
private getAllColumns(): Column[] {
|
|
1272
2097
|
return Array.from(this.columns.values());
|
|
1273
2098
|
}
|
|
1274
|
-
|
|
2099
|
+
|
|
1275
2100
|
private downloadFile(content: string, fileName: string, mimeType: string): void {
|
|
1276
2101
|
const blob = new Blob([content], { type: mimeType });
|
|
1277
2102
|
const url = URL.createObjectURL(blob);
|
|
@@ -1281,4 +2106,148 @@ export class GridService<TData = any> {
|
|
|
1281
2106
|
link.click();
|
|
1282
2107
|
URL.revokeObjectURL(url);
|
|
1283
2108
|
}
|
|
2109
|
+
|
|
2110
|
+
// ============================================================================
|
|
2111
|
+
// STATE PERSISTENCE API
|
|
2112
|
+
// ============================================================================
|
|
2113
|
+
|
|
2114
|
+
/**
|
|
2115
|
+
* Get current grid state
|
|
2116
|
+
* Includes: columns (order, size, visibility, pinning), filters, sort, grouping
|
|
2117
|
+
*/
|
|
2118
|
+
getState(): GridState {
|
|
2119
|
+
const columns = this.getAllColumns();
|
|
2120
|
+
|
|
2121
|
+
return {
|
|
2122
|
+
columnOrder: columns.map((col) => ({
|
|
2123
|
+
colId: col.colId,
|
|
2124
|
+
width: col.width,
|
|
2125
|
+
hide: !col.visible,
|
|
2126
|
+
pinned: col.pinned,
|
|
2127
|
+
sort: col.sort,
|
|
2128
|
+
sortIndex: col.sortIndex,
|
|
2129
|
+
aggFunc: col.aggFunc,
|
|
2130
|
+
})),
|
|
2131
|
+
filter: { ...this.filterModel },
|
|
2132
|
+
sort: { sortModel: [...this.sortModel] },
|
|
2133
|
+
rowGrouping: {
|
|
2134
|
+
rowGroupCols: this.getGroupColumns(),
|
|
2135
|
+
valueCols: [],
|
|
2136
|
+
pivotCols: [],
|
|
2137
|
+
isPivotMode: false,
|
|
2138
|
+
},
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/**
|
|
2143
|
+
* Restore grid state
|
|
2144
|
+
* Applies column state, filters, sort, and grouping
|
|
2145
|
+
*/
|
|
2146
|
+
setState(state: GridState): void {
|
|
2147
|
+
// Restore column state
|
|
2148
|
+
if (state.columnOrder) {
|
|
2149
|
+
const newColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
|
|
2150
|
+
|
|
2151
|
+
state.columnOrder.forEach((colState) => {
|
|
2152
|
+
const colDef = this.columnDefs?.find((d) =>
|
|
2153
|
+
'children' in d
|
|
2154
|
+
? false
|
|
2155
|
+
: (d as ColDef<TData>).colId === colState.colId ||
|
|
2156
|
+
(d as ColDef<TData>).field === colState.colId
|
|
2157
|
+
);
|
|
2158
|
+
|
|
2159
|
+
if (colDef && !('children' in colDef)) {
|
|
2160
|
+
newColumnDefs.push({
|
|
2161
|
+
...colDef,
|
|
2162
|
+
width: colState.width,
|
|
2163
|
+
hide: colState.hide,
|
|
2164
|
+
pinned: colState.pinned || false,
|
|
2165
|
+
sort: colState.sort,
|
|
2166
|
+
sortIndex: colState.sortIndex,
|
|
2167
|
+
rowGroup: colState.rowGroupIndex !== undefined,
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
if (newColumnDefs.length > 0) {
|
|
2173
|
+
this.columnDefs = newColumnDefs;
|
|
2174
|
+
this.initializeColumns();
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Restore filters
|
|
2179
|
+
if (state.filter) {
|
|
2180
|
+
this.filterModel = { ...state.filter };
|
|
2181
|
+
this.applyFiltering();
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// Restore sort
|
|
2185
|
+
if (state.sort?.sortModel && state.sort.sortModel.length > 0) {
|
|
2186
|
+
this.sortModel = [...state.sort.sortModel];
|
|
2187
|
+
this.applyFiltering(); // This will trigger sorting
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Restore row grouping (handled through column state)
|
|
2191
|
+
// Row grouping is applied when columns are restored with rowGroup: true
|
|
2192
|
+
|
|
2193
|
+
// Emit state change event
|
|
2194
|
+
this.gridStateChanged$.next({ type: 'state-restored' });
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* Save grid state to LocalStorage
|
|
2199
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
2200
|
+
*/
|
|
2201
|
+
saveState(key: string = 'argent-grid-state'): void {
|
|
2202
|
+
try {
|
|
2203
|
+
const state = this.getState();
|
|
2204
|
+
localStorage.setItem(key, JSON.stringify(state));
|
|
2205
|
+
this.gridStateChanged$.next({ type: 'state-saved', key });
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
console.warn('Failed to save grid state:', error);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* Restore grid state from LocalStorage
|
|
2213
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
2214
|
+
* @returns true if state was restored, false if no state found
|
|
2215
|
+
*/
|
|
2216
|
+
restoreState(key: string = 'argent-grid-state'): boolean {
|
|
2217
|
+
try {
|
|
2218
|
+
const stateJson = localStorage.getItem(key);
|
|
2219
|
+
if (!stateJson) {
|
|
2220
|
+
return false;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const state: GridState = JSON.parse(stateJson);
|
|
2224
|
+
this.setState(state);
|
|
2225
|
+
this.gridStateChanged$.next({ type: 'state-restored', key });
|
|
2226
|
+
return true;
|
|
2227
|
+
} catch (error) {
|
|
2228
|
+
console.warn('Failed to restore grid state:', error);
|
|
2229
|
+
return false;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Clear grid state from LocalStorage
|
|
2235
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
2236
|
+
*/
|
|
2237
|
+
clearState(key: string = 'argent-grid-state'): void {
|
|
2238
|
+
try {
|
|
2239
|
+
localStorage.removeItem(key);
|
|
2240
|
+
this.gridStateChanged$.next({ type: 'state-cleared', key });
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
console.warn('Failed to clear grid state:', error);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
/**
|
|
2247
|
+
* Check if state exists in LocalStorage
|
|
2248
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
2249
|
+
*/
|
|
2250
|
+
hasState(key: string = 'argent-grid-state'): boolean {
|
|
2251
|
+
return localStorage.getItem(key) !== null;
|
|
2252
|
+
}
|
|
1284
2253
|
}
|