argent-grid 0.1.0 → 0.2.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 +2 -2
- 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/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +91 -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/package.json +20 -6
- package/plan.md +50 -18
- 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 +327 -76
- package/src/lib/components/argent-grid.component.html +186 -64
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +642 -189
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.ts +302 -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 +366 -0
- package/src/lib/rendering/canvas-renderer.ts +418 -305
- 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 +46 -24
- package/src/lib/rendering/render/column-utils.ts +73 -0
- package/src/lib/rendering/render/hit-test.ts +55 -0
- package/src/lib/rendering/render/index.ts +79 -76
- package/src/lib/rendering/render/lines.ts +43 -43
- package/src/lib/rendering/render/primitives.ts +161 -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 +2 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +60 -50
- 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 +1165 -201
- package/src/lib/services/grid.service.ts +819 -187
- 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 +73 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +188 -0
- package/src/stories/ArgentGrid.stories.ts +277 -0
- package/src/stories/Benchmark.stories.ts +74 -0
- package/src/stories/CellRenderers.stories.ts +221 -0
- package/src/stories/Filtering.stories.ts +252 -0
- package/src/stories/Grouping.stories.ts +217 -0
- package/src/stories/Theming.stories.ts +124 -0
- package/src/stories/benchmark-wrapper.component.ts +315 -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,22 +1,23 @@
|
|
|
1
|
-
import { Injectable
|
|
2
|
-
import { Subject } from 'rxjs';
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
3
2
|
import { Workbook } from 'exceljs';
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { Subject } from 'rxjs';
|
|
4
|
+
import {
|
|
5
|
+
CellRange,
|
|
6
6
|
ColDef,
|
|
7
7
|
ColGroupDef,
|
|
8
8
|
Column,
|
|
9
|
-
|
|
9
|
+
CsvExportParams,
|
|
10
|
+
ExcelExportParams,
|
|
10
11
|
FilterModel,
|
|
11
12
|
FilterModelItem,
|
|
12
|
-
|
|
13
|
+
GridApi,
|
|
14
|
+
GridOptions,
|
|
13
15
|
GridState,
|
|
16
|
+
GroupRowNode,
|
|
17
|
+
IRowNode,
|
|
14
18
|
RowDataTransaction,
|
|
15
19
|
RowDataTransactionResult,
|
|
16
|
-
|
|
17
|
-
ExcelExportParams,
|
|
18
|
-
GroupRowNode,
|
|
19
|
-
CellRange
|
|
20
|
+
SortModelItem,
|
|
20
21
|
} from '../types/ag-grid-types';
|
|
21
22
|
|
|
22
23
|
@Injectable()
|
|
@@ -34,7 +35,7 @@ export class GridService<TData = any> {
|
|
|
34
35
|
private cellRanges: CellRange[] = [];
|
|
35
36
|
private gridId: string = '';
|
|
36
37
|
private gridOptions: GridOptions<TData> | null = null;
|
|
37
|
-
public gridStateChanged$ = new Subject<{ type: string
|
|
38
|
+
public gridStateChanged$ = new Subject<{ type: string; key?: string; value?: any }>();
|
|
38
39
|
|
|
39
40
|
// Row height cache
|
|
40
41
|
private cumulativeRowHeights: number[] = [];
|
|
@@ -47,7 +48,7 @@ export class GridService<TData = any> {
|
|
|
47
48
|
// Pivoting state
|
|
48
49
|
private pivotColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
|
|
49
50
|
private isPivotMode = false;
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
createApi(
|
|
52
53
|
columnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null,
|
|
53
54
|
rowData: TData[] | null,
|
|
@@ -62,31 +63,64 @@ export class GridService<TData = any> {
|
|
|
62
63
|
this.isPivotMode = !!this.gridOptions.pivotMode;
|
|
63
64
|
|
|
64
65
|
this.initializeColumns();
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
// Trigger initial pipeline run
|
|
67
68
|
this.applySorting();
|
|
68
69
|
this.applyFiltering(); // This will trigger grouping if needed and initialize nodes
|
|
69
70
|
|
|
70
71
|
return this.createGridApi();
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
|
|
73
74
|
private generateGridId(): string {
|
|
74
75
|
return `argent-grid-${Math.random().toString(36).substr(2, 9)}`;
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
|
|
78
|
+
private hasCheckboxSelection(): boolean {
|
|
79
|
+
if (this.gridOptions?.selectionColumnDef?.checkboxes) return true;
|
|
80
|
+
if (!this.columnDefs) return false;
|
|
81
|
+
|
|
82
|
+
const check = (defs: (ColDef | ColGroupDef)[]): boolean => {
|
|
83
|
+
return defs.some((def) => {
|
|
84
|
+
if ('children' in def) return check(def.children);
|
|
85
|
+
return !!def.checkboxSelection;
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return check(this.columnDefs);
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
private initializeColumns(): void {
|
|
78
93
|
if (!this.columnDefs) {
|
|
79
94
|
return;
|
|
80
95
|
}
|
|
81
|
-
|
|
96
|
+
|
|
82
97
|
this.columns.clear();
|
|
83
98
|
|
|
84
99
|
const groupColumns = this.getGroupColumns();
|
|
85
100
|
const isGrouping = groupColumns.length > 0;
|
|
86
101
|
const groupDisplayType = this.gridOptions?.groupDisplayType || 'singleColumn';
|
|
87
102
|
|
|
88
|
-
// 1. Handle
|
|
89
|
-
if (
|
|
103
|
+
// 1. Handle Selection Column
|
|
104
|
+
if (this.hasCheckboxSelection()) {
|
|
105
|
+
const selectionCol: Column = {
|
|
106
|
+
colId: 'ag-Grid-SelectionColumn',
|
|
107
|
+
field: 'ag-Grid-SelectionColumn',
|
|
108
|
+
headerName: '',
|
|
109
|
+
width: 50,
|
|
110
|
+
pinned: 'left',
|
|
111
|
+
visible: true,
|
|
112
|
+
sort: null,
|
|
113
|
+
checkboxSelection: true,
|
|
114
|
+
headerCheckboxSelection: true,
|
|
115
|
+
};
|
|
116
|
+
this.columns.set(selectionCol.colId, selectionCol);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 2. Handle Auto Group Column (for singleColumn display)
|
|
120
|
+
if (
|
|
121
|
+
isGrouping &&
|
|
122
|
+
(groupDisplayType === 'singleColumn' || !this.gridOptions?.groupDisplayType)
|
|
123
|
+
) {
|
|
90
124
|
const autoGroupDef = this.gridOptions?.autoGroupColumnDef || {};
|
|
91
125
|
const autoGroupCol: Column = {
|
|
92
126
|
colId: 'ag-Grid-AutoColumn',
|
|
@@ -97,37 +131,43 @@ export class GridService<TData = any> {
|
|
|
97
131
|
maxWidth: autoGroupDef.maxWidth,
|
|
98
132
|
pinned: this.normalizePinned(autoGroupDef.pinned || 'left'),
|
|
99
133
|
visible: true,
|
|
100
|
-
sort: null
|
|
134
|
+
sort: null,
|
|
101
135
|
};
|
|
102
136
|
this.columns.set(autoGroupCol.colId, autoGroupCol);
|
|
103
137
|
}
|
|
104
138
|
|
|
105
139
|
// 2. Process regular columns
|
|
106
|
-
const columnsToProcess =
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
const columnsToProcess =
|
|
141
|
+
this.isPivotMode && this.pivotColumnDefs
|
|
142
|
+
? [...this.columnDefs, ...this.pivotColumnDefs]
|
|
143
|
+
: this.columnDefs;
|
|
109
144
|
|
|
110
145
|
columnsToProcess.forEach((def, index) => {
|
|
111
146
|
if ('children' in def) {
|
|
112
147
|
// Column group
|
|
113
148
|
def.children.forEach((child, childIndex) => {
|
|
114
|
-
|
|
149
|
+
// Merge defaultColDef for nested columns too
|
|
150
|
+
const mergedChild = { ...this.gridOptions?.defaultColDef, ...child };
|
|
151
|
+
this.addColumn(mergedChild, index * 100 + childIndex, isGrouping);
|
|
115
152
|
});
|
|
116
153
|
} else {
|
|
117
|
-
this.
|
|
154
|
+
const mergedDef = { ...this.gridOptions?.defaultColDef, ...def };
|
|
155
|
+
this.addColumn(mergedDef, index, isGrouping);
|
|
118
156
|
}
|
|
119
157
|
});
|
|
120
158
|
}
|
|
121
159
|
|
|
122
|
-
private normalizePinned(
|
|
160
|
+
private normalizePinned(
|
|
161
|
+
pinned: boolean | 'left' | 'right' | null | undefined
|
|
162
|
+
): 'left' | 'right' | false {
|
|
123
163
|
if (pinned === 'left' || pinned === true) return 'left';
|
|
124
164
|
if (pinned === 'right') return 'right';
|
|
125
165
|
return false;
|
|
126
166
|
}
|
|
127
|
-
|
|
167
|
+
|
|
128
168
|
private addColumn(def: ColDef<TData>, index: number, isGrouping: boolean): void {
|
|
129
169
|
const colId = def.colId || def.field?.toString() || `col-${index}`;
|
|
130
|
-
|
|
170
|
+
|
|
131
171
|
// Auto-hide columns that are being grouped (AG Grid default)
|
|
132
172
|
let visible = !def.hide;
|
|
133
173
|
if (isGrouping && def.rowGroup && visible && this.gridOptions?.groupHideOpenParents !== false) {
|
|
@@ -145,7 +185,13 @@ export class GridService<TData = any> {
|
|
|
145
185
|
}
|
|
146
186
|
|
|
147
187
|
// In pivot mode, hide columns that are not part of grouping or pivot results
|
|
148
|
-
if (
|
|
188
|
+
if (
|
|
189
|
+
this.isPivotMode &&
|
|
190
|
+
visible &&
|
|
191
|
+
!def.rowGroup &&
|
|
192
|
+
!colId.startsWith('pivot_') &&
|
|
193
|
+
colId !== 'ag-Grid-AutoColumn'
|
|
194
|
+
) {
|
|
149
195
|
visible = false;
|
|
150
196
|
}
|
|
151
197
|
|
|
@@ -158,13 +204,19 @@ export class GridService<TData = any> {
|
|
|
158
204
|
maxWidth: def.maxWidth,
|
|
159
205
|
pinned: this.normalizePinned(def.pinned),
|
|
160
206
|
visible: visible,
|
|
161
|
-
sort:
|
|
207
|
+
sort:
|
|
208
|
+
typeof def.sort === 'object' && def.sort !== null
|
|
209
|
+
? (def.sort as any).sort
|
|
210
|
+
: def.sort || null,
|
|
162
211
|
sortIndex: def.sortIndex ?? undefined,
|
|
163
|
-
aggFunc: typeof def.aggFunc === 'string' ? def.aggFunc : null
|
|
212
|
+
aggFunc: typeof def.aggFunc === 'string' ? def.aggFunc : null,
|
|
213
|
+
checkboxSelection: !!def.checkboxSelection,
|
|
214
|
+
headerCheckboxSelection: !!def.headerCheckboxSelection,
|
|
215
|
+
filter: def.filter,
|
|
164
216
|
};
|
|
165
217
|
this.columns.set(colId, column);
|
|
166
218
|
}
|
|
167
|
-
|
|
219
|
+
|
|
168
220
|
private getRowId(data: TData, index: number): string {
|
|
169
221
|
// 1. Try custom callback from gridOptions
|
|
170
222
|
if (this.gridOptions?.getRowId) {
|
|
@@ -175,9 +227,11 @@ export class GridService<TData = any> {
|
|
|
175
227
|
const anyData = data as any;
|
|
176
228
|
return anyData?.id?.toString() || anyData?.Id?.toString() || `row-${index}`;
|
|
177
229
|
}
|
|
178
|
-
|
|
230
|
+
|
|
179
231
|
private createGridApi(): GridApi<TData> {
|
|
180
|
-
|
|
232
|
+
const api: any = {};
|
|
233
|
+
|
|
234
|
+
Object.assign(api, {
|
|
181
235
|
// Column API
|
|
182
236
|
getColumnDefs: () => this.columnDefs,
|
|
183
237
|
setColumnDefs: (colDefs) => {
|
|
@@ -193,7 +247,7 @@ export class GridService<TData = any> {
|
|
|
193
247
|
getDisplayedRowAtIndex: (index) => {
|
|
194
248
|
return this.displayedRowNodes[index] || null;
|
|
195
249
|
},
|
|
196
|
-
|
|
250
|
+
|
|
197
251
|
// Row Data API
|
|
198
252
|
getRowData: () => [...this.filteredRowData],
|
|
199
253
|
setRowData: (rowData) => {
|
|
@@ -207,25 +261,25 @@ export class GridService<TData = any> {
|
|
|
207
261
|
getDisplayedRowCount: () => this.displayedRowNodes.length,
|
|
208
262
|
getAggregations: () => this.calculateColumnAggregations(this.filteredRowData),
|
|
209
263
|
getRowNode: (id) => this.rowNodes.get(id) || null,
|
|
210
|
-
|
|
264
|
+
|
|
211
265
|
// Selection API
|
|
212
|
-
getSelectedRows: () => Array.from(this.rowNodes.values()).filter(n => n.selected),
|
|
213
|
-
getSelectedNodes: () => Array.from(this.rowNodes.values()).filter(n => n.selected),
|
|
266
|
+
getSelectedRows: () => Array.from(this.rowNodes.values()).filter((n) => n.selected),
|
|
267
|
+
getSelectedNodes: () => Array.from(this.rowNodes.values()).filter((n) => n.selected),
|
|
214
268
|
selectAll: () => {
|
|
215
|
-
this.rowNodes.forEach(node => {
|
|
269
|
+
this.rowNodes.forEach((node) => {
|
|
216
270
|
node.selected = true;
|
|
217
271
|
this.selectedRows.add(node.id!);
|
|
218
272
|
});
|
|
219
273
|
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
220
274
|
},
|
|
221
275
|
deselectAll: () => {
|
|
222
|
-
this.rowNodes.forEach(node => {
|
|
276
|
+
this.rowNodes.forEach((node) => {
|
|
223
277
|
node.selected = false;
|
|
224
278
|
});
|
|
225
279
|
this.selectedRows.clear();
|
|
226
280
|
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
227
281
|
},
|
|
228
|
-
|
|
282
|
+
|
|
229
283
|
// Filter API
|
|
230
284
|
setFilterModel: (model) => {
|
|
231
285
|
this.filterModel = model;
|
|
@@ -238,7 +292,7 @@ export class GridService<TData = any> {
|
|
|
238
292
|
this.gridStateChanged$.next({ type: 'filterChanged' });
|
|
239
293
|
},
|
|
240
294
|
isFilterPresent: () => Object.keys(this.filterModel).length > 0,
|
|
241
|
-
|
|
295
|
+
|
|
242
296
|
// Sort API
|
|
243
297
|
setSortModel: (model) => {
|
|
244
298
|
this.sortModel = model;
|
|
@@ -252,7 +306,7 @@ export class GridService<TData = any> {
|
|
|
252
306
|
this.applyFiltering(); // Re-filter and re-group after sort
|
|
253
307
|
this.gridStateChanged$.next({ type: 'sortChanged' });
|
|
254
308
|
},
|
|
255
|
-
|
|
309
|
+
|
|
256
310
|
// Pagination API
|
|
257
311
|
paginationGetPageSize: () => 100,
|
|
258
312
|
paginationSetPageSize: () => {},
|
|
@@ -262,49 +316,78 @@ export class GridService<TData = any> {
|
|
|
262
316
|
paginationGoToLastPage: () => {},
|
|
263
317
|
paginationGoToNextPage: () => {},
|
|
264
318
|
paginationGoToPreviousPage: () => {},
|
|
265
|
-
|
|
319
|
+
|
|
266
320
|
// Export API
|
|
267
|
-
exportDataAsCsv: (params) => this.exportAsCsv(params),
|
|
321
|
+
exportDataAsCsv: (params) => this.exportAsCsv(params, api),
|
|
268
322
|
exportDataAsExcel: (params) => this.exportAsExcel(params),
|
|
269
|
-
|
|
323
|
+
downloadFile: (content, fileName, mimeType) => this.downloadFile(content, fileName, mimeType),
|
|
324
|
+
|
|
270
325
|
// Clipboard API
|
|
271
|
-
copyToClipboard: () => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
326
|
+
copyToClipboard: () => {
|
|
327
|
+
try {
|
|
328
|
+
const selectedData = api.getSelectedRows().map((node) => node.data);
|
|
329
|
+
const csv = selectedData.map((row) => Object.values(row as any).join(',')).join('\n');
|
|
330
|
+
if (typeof navigator !== 'undefined' && (navigator as any).clipboard) {
|
|
331
|
+
(navigator as any).clipboard.writeText(csv);
|
|
332
|
+
}
|
|
333
|
+
} catch (_e) {
|
|
334
|
+
// Ignore clipboard errors
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
cutToClipboard: () => {
|
|
338
|
+
api.copyToClipboard();
|
|
339
|
+
},
|
|
340
|
+
pasteFromClipboard: () => {
|
|
341
|
+
try {
|
|
342
|
+
if (typeof navigator !== 'undefined' && (navigator as any).clipboard) {
|
|
343
|
+
(navigator as any).clipboard.readText();
|
|
344
|
+
}
|
|
345
|
+
} catch (_e) {
|
|
346
|
+
// Ignore clipboard errors
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
|
|
275
350
|
// Grid State API
|
|
276
351
|
getState: () => this.getGridState(),
|
|
277
352
|
applyState: (state) => this.applyGridState(state),
|
|
278
|
-
|
|
353
|
+
|
|
354
|
+
// State Persistence API
|
|
355
|
+
saveState: (key?: string) => this.saveState(key),
|
|
356
|
+
restoreState: (key?: string) => this.restoreState(key),
|
|
357
|
+
clearState: (key?: string) => this.clearState(key),
|
|
358
|
+
hasState: (key?: string) => this.hasState(key),
|
|
359
|
+
setState: (state: GridState) => this.setState(state),
|
|
360
|
+
getUniqueValues: (field: string) => this.getUniqueValues(field),
|
|
361
|
+
|
|
279
362
|
// Focus API
|
|
280
363
|
setFocusedCell: () => {},
|
|
281
364
|
getFocusedCell: () => null,
|
|
282
|
-
|
|
365
|
+
|
|
283
366
|
// Refresh API
|
|
284
367
|
refreshCells: () => {},
|
|
285
368
|
refreshRows: (params) => {
|
|
286
369
|
if (params?.rowNodes) {
|
|
287
|
-
params.rowNodes.forEach(
|
|
370
|
+
params.rowNodes.forEach((_node) => {
|
|
288
371
|
// Trigger cell refresh
|
|
289
372
|
});
|
|
290
373
|
}
|
|
291
374
|
},
|
|
292
375
|
refreshHeader: () => {},
|
|
293
|
-
|
|
376
|
+
|
|
294
377
|
// Scroll API
|
|
295
378
|
ensureIndexVisible: () => {},
|
|
296
379
|
ensureColumnVisible: () => {},
|
|
297
|
-
|
|
380
|
+
|
|
298
381
|
// Destroy API
|
|
299
382
|
destroy: () => {
|
|
300
383
|
this.columns.clear();
|
|
301
384
|
this.rowNodes.clear();
|
|
302
385
|
this.rowData = [];
|
|
303
386
|
},
|
|
304
|
-
|
|
387
|
+
|
|
305
388
|
// Grid Information
|
|
306
389
|
getGridId: () => this.gridId,
|
|
307
|
-
getGridOption: (key) => this.gridOptions ? this.gridOptions[key] : undefined as any,
|
|
390
|
+
getGridOption: (key) => (this.gridOptions ? this.gridOptions[key] : (undefined as any)),
|
|
308
391
|
setGridOption: (key, value) => {
|
|
309
392
|
if (!this.gridOptions) {
|
|
310
393
|
this.gridOptions = {} as GridOptions<TData>;
|
|
@@ -312,7 +395,7 @@ export class GridService<TData = any> {
|
|
|
312
395
|
this.gridOptions[key] = value;
|
|
313
396
|
this.gridStateChanged$.next({ type: 'optionChanged', key: key as string, value });
|
|
314
397
|
},
|
|
315
|
-
|
|
398
|
+
|
|
316
399
|
// Group Expansion
|
|
317
400
|
setRowNodeExpanded: (node, expanded) => {
|
|
318
401
|
if (node.id && (node.group || node.master)) {
|
|
@@ -321,13 +404,13 @@ export class GridService<TData = any> {
|
|
|
321
404
|
} else {
|
|
322
405
|
this.expandedGroups.delete(node.id);
|
|
323
406
|
}
|
|
324
|
-
|
|
407
|
+
|
|
325
408
|
if (node.group) {
|
|
326
409
|
this.applyGrouping();
|
|
327
410
|
} else {
|
|
328
411
|
this.initializeRowNodesFromFilteredData();
|
|
329
412
|
}
|
|
330
|
-
|
|
413
|
+
|
|
331
414
|
this.gridStateChanged$.next({ type: 'groupExpanded', value: expanded });
|
|
332
415
|
}
|
|
333
416
|
},
|
|
@@ -349,9 +432,9 @@ export class GridService<TData = any> {
|
|
|
349
432
|
}
|
|
350
433
|
},
|
|
351
434
|
isPivotMode: () => this.isPivotMode,
|
|
352
|
-
|
|
435
|
+
|
|
353
436
|
// Range Selection API
|
|
354
|
-
getCellRanges: () => this.cellRanges.length > 0 ? [...this.cellRanges] : null,
|
|
437
|
+
getCellRanges: () => (this.cellRanges.length > 0 ? [...this.cellRanges] : null),
|
|
355
438
|
addCellRange: (range) => {
|
|
356
439
|
this.cellRanges = [range]; // For now only support single range
|
|
357
440
|
this.gridStateChanged$.next({ type: 'rangeSelectionChanged' });
|
|
@@ -361,38 +444,342 @@ export class GridService<TData = any> {
|
|
|
361
444
|
this.cellRanges = [];
|
|
362
445
|
this.gridStateChanged$.next({ type: 'rangeSelectionChanged' });
|
|
363
446
|
}
|
|
364
|
-
}
|
|
365
|
-
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
// Column Operations
|
|
450
|
+
moveColumn: (column, toIndex) => {
|
|
451
|
+
// Basic implementation - reorder columns array
|
|
452
|
+
const cols = Array.from(this.columns.values());
|
|
453
|
+
const idx = cols.findIndex((c) => c.colId === column.colId);
|
|
454
|
+
if (idx !== -1) {
|
|
455
|
+
cols.splice(idx, 1);
|
|
456
|
+
cols.splice(toIndex, 0, column);
|
|
457
|
+
this.columns.clear();
|
|
458
|
+
cols.forEach((c) => {
|
|
459
|
+
this.columns.set(c.colId, c);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
setColumnWidth: (column, width) => {
|
|
464
|
+
if (column) {
|
|
465
|
+
column.width = width;
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
setColumnPinned: (column, pinned) => {
|
|
469
|
+
if (column) {
|
|
470
|
+
column.pinned = pinned === true ? 'left' : pinned;
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
setColumnVisible: (column, visible) => {
|
|
474
|
+
if (column) {
|
|
475
|
+
column.visible = visible;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
setColumnSort: (column, sort, multiSort) => {
|
|
479
|
+
if (column) {
|
|
480
|
+
column.sort = sort;
|
|
481
|
+
column.sortIndex = multiSort ? 0 : undefined;
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
autoSizeColumns: (colKeys) => {
|
|
485
|
+
// Basic implementation - set reasonable widths
|
|
486
|
+
colKeys.forEach((key) => {
|
|
487
|
+
const col = typeof key === 'string' ? this.columns.get(key) : key;
|
|
488
|
+
if (col) {
|
|
489
|
+
col.width = 150;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
},
|
|
493
|
+
getColumnState: () => {
|
|
494
|
+
return Array.from(this.columns.values()).map((col) => ({
|
|
495
|
+
colId: col.colId,
|
|
496
|
+
width: col.width,
|
|
497
|
+
hide: !col.visible,
|
|
498
|
+
pinned: col.pinned,
|
|
499
|
+
sort: col.sort,
|
|
500
|
+
sortIndex: col.sortIndex,
|
|
501
|
+
}));
|
|
502
|
+
},
|
|
503
|
+
applyColumnState: (state) => {
|
|
504
|
+
if (Array.isArray(state)) {
|
|
505
|
+
state.forEach((colState) => {
|
|
506
|
+
const col = this.columns.get(colState.colId);
|
|
507
|
+
if (col) {
|
|
508
|
+
col.width = colState.width;
|
|
509
|
+
col.visible = !colState.hide;
|
|
510
|
+
col.pinned = colState.pinned;
|
|
511
|
+
col.sort = colState.sort;
|
|
512
|
+
col.sortIndex = colState.sortIndex;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
resetColumnState: () => {
|
|
518
|
+
// Reset to initial state
|
|
519
|
+
if (this.columnDefs) {
|
|
520
|
+
this.initializeColumns();
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
// Cell Editing
|
|
525
|
+
startEditingCell: (_params) => {
|
|
526
|
+
// Basic implementation
|
|
527
|
+
},
|
|
528
|
+
stopEditing: () => {
|
|
529
|
+
// Basic implementation
|
|
530
|
+
},
|
|
531
|
+
getEditingCells: () => {
|
|
532
|
+
return [];
|
|
533
|
+
},
|
|
534
|
+
flashCells: (_params) => {
|
|
535
|
+
// Basic implementation
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
// Row Operations
|
|
539
|
+
resetRowHeights: () => {
|
|
540
|
+
this.updateRowHeightCache();
|
|
541
|
+
},
|
|
542
|
+
getRowHeightForRow: (rowIndex) => {
|
|
543
|
+
const node = this.displayedRowNodes[rowIndex];
|
|
544
|
+
return node?.rowHeight || this.gridOptions?.rowHeight || 32;
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
// Scroll Operations
|
|
548
|
+
setScrollPosition: (_params) => {
|
|
549
|
+
// Basic implementation
|
|
550
|
+
},
|
|
551
|
+
getScrollPosition: () => {
|
|
552
|
+
return { top: 0, left: 0 };
|
|
553
|
+
},
|
|
554
|
+
sizeColumnsToFit: (width) => {
|
|
555
|
+
const cols = Array.from(this.columns.values()).filter((c) => c.visible);
|
|
556
|
+
if (cols.length > 0) {
|
|
557
|
+
const colWidth = Math.floor(width / cols.length);
|
|
558
|
+
cols.forEach((col) => (col.width = colWidth));
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
// Pivot Mode
|
|
563
|
+
getPivotColumns: () => {
|
|
564
|
+
return [];
|
|
565
|
+
},
|
|
566
|
+
getValueColumns: () => {
|
|
567
|
+
return [];
|
|
568
|
+
},
|
|
569
|
+
getRowGroupColumns: () => {
|
|
570
|
+
return this.getGroupColumns();
|
|
571
|
+
},
|
|
572
|
+
getGroupDisplayType: () => {
|
|
573
|
+
return this.gridOptions?.groupDisplayType || 'singleColumn';
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
// Tool Panels
|
|
577
|
+
setSideBarVisible: (_visible) => {
|
|
578
|
+
// Basic implementation
|
|
579
|
+
},
|
|
580
|
+
openToolPanel: (_panelId) => {
|
|
581
|
+
// Basic implementation
|
|
582
|
+
},
|
|
583
|
+
closeToolPanel: () => {
|
|
584
|
+
// Basic implementation
|
|
585
|
+
},
|
|
586
|
+
enableFilterToolPanel: () => {
|
|
587
|
+
// Basic implementation
|
|
588
|
+
},
|
|
589
|
+
enableColumnsToolPanel: () => {
|
|
590
|
+
// Basic implementation
|
|
591
|
+
},
|
|
592
|
+
getToolPanel: (_panelId) => {
|
|
593
|
+
return null;
|
|
594
|
+
},
|
|
595
|
+
isToolPanelShowing: () => {
|
|
596
|
+
return false;
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
// Context Menu
|
|
600
|
+
getContextMenuItems: () => {
|
|
601
|
+
return [];
|
|
602
|
+
},
|
|
603
|
+
getMainMenuItems: () => {
|
|
604
|
+
return [];
|
|
605
|
+
},
|
|
606
|
+
getHeaderContextMenuItems: () => {
|
|
607
|
+
return [];
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
// Event Handling
|
|
611
|
+
addEventListener: (_eventType, _listener) => {
|
|
612
|
+
// Basic implementation
|
|
613
|
+
},
|
|
614
|
+
removeEventListener: (_eventType, _listener) => {
|
|
615
|
+
// Basic implementation
|
|
616
|
+
},
|
|
617
|
+
dispatchEvent: (_event) => {
|
|
618
|
+
// Basic implementation
|
|
619
|
+
},
|
|
620
|
+
getEventPath: () => {
|
|
621
|
+
return [];
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
// Rendering
|
|
625
|
+
getRenderedNodes: () => {
|
|
626
|
+
return [...this.displayedRowNodes];
|
|
627
|
+
},
|
|
628
|
+
getFirstRenderedRow: () => {
|
|
629
|
+
return 0;
|
|
630
|
+
},
|
|
631
|
+
getLastRenderedRow: () => {
|
|
632
|
+
return this.displayedRowNodes.length - 1;
|
|
633
|
+
},
|
|
634
|
+
getVerticalPixelRange: () => {
|
|
635
|
+
return { start: 0, end: this.getTotalHeight() };
|
|
636
|
+
},
|
|
637
|
+
getHorizontalPixelRange: () => {
|
|
638
|
+
const cols = Array.from(this.columns.values()).filter((c) => c.visible);
|
|
639
|
+
const width = cols.reduce((sum, col) => sum + col.width, 0);
|
|
640
|
+
return { start: 0, end: width };
|
|
641
|
+
},
|
|
642
|
+
getPinnedWidth: () => {
|
|
643
|
+
const leftPinned = Array.from(this.columns.values()).filter((c) => c.pinned === 'left');
|
|
644
|
+
return leftPinned.reduce((sum, col) => sum + col.width, 0);
|
|
645
|
+
},
|
|
646
|
+
getRightPinnedWidth: () => {
|
|
647
|
+
const rightPinned = Array.from(this.columns.values()).filter((c) => c.pinned === 'right');
|
|
648
|
+
return rightPinned.reduce((sum, col) => sum + col.width, 0);
|
|
649
|
+
},
|
|
650
|
+
getHScrollPosition: () => {
|
|
651
|
+
return 0;
|
|
652
|
+
},
|
|
653
|
+
getVScrollPosition: () => {
|
|
654
|
+
return 0;
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
// Localization
|
|
658
|
+
getLocaleText: () => {
|
|
659
|
+
return '';
|
|
660
|
+
},
|
|
661
|
+
setLocaleText: (_locale, _texts) => {
|
|
662
|
+
// Basic implementation
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
// Charts
|
|
666
|
+
getChartModels: () => {
|
|
667
|
+
return [];
|
|
668
|
+
},
|
|
669
|
+
getChartToolbarItems: () => {
|
|
670
|
+
return [];
|
|
671
|
+
},
|
|
672
|
+
hidePopup: () => {
|
|
673
|
+
// Basic implementation
|
|
674
|
+
},
|
|
675
|
+
getSparklineOptions: () => {
|
|
676
|
+
return [];
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
// Grid State
|
|
680
|
+
getGridPanel: () => {
|
|
681
|
+
return null;
|
|
682
|
+
},
|
|
683
|
+
getRowContainerElement: () => {
|
|
684
|
+
return null;
|
|
685
|
+
},
|
|
686
|
+
getBodyElement: () => {
|
|
687
|
+
return null;
|
|
688
|
+
},
|
|
689
|
+
getHeaderElements: () => {
|
|
690
|
+
return [];
|
|
691
|
+
},
|
|
692
|
+
getCenterElements: () => {
|
|
693
|
+
return [];
|
|
694
|
+
},
|
|
695
|
+
getLeftElements: () => {
|
|
696
|
+
return [];
|
|
697
|
+
},
|
|
698
|
+
getRightElements: () => {
|
|
699
|
+
return [];
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
// Disabled State
|
|
703
|
+
setDisabled: (_disabled) => {
|
|
704
|
+
// Basic implementation
|
|
705
|
+
},
|
|
706
|
+
isDisabled: () => {
|
|
707
|
+
return false;
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
// Row Information
|
|
711
|
+
getRowPosition: (rowIndex) => {
|
|
712
|
+
return this.getRowY(rowIndex);
|
|
713
|
+
},
|
|
714
|
+
getRowStyle: (_rowIndex) => {
|
|
715
|
+
return null;
|
|
716
|
+
},
|
|
717
|
+
getRowClass: (_rowIndex) => {
|
|
718
|
+
return null;
|
|
719
|
+
},
|
|
720
|
+
getRowId: (node) => {
|
|
721
|
+
return node.id;
|
|
722
|
+
},
|
|
723
|
+
isRowMaster: (node) => {
|
|
724
|
+
return !!node.master;
|
|
725
|
+
},
|
|
726
|
+
getColumnGroups: () => {
|
|
727
|
+
return [];
|
|
728
|
+
},
|
|
729
|
+
getColumnGroup: () => {
|
|
730
|
+
return null;
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
// Aggregation
|
|
734
|
+
refreshAggregatedCols: () => {
|
|
735
|
+
// Basic implementation
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
// ForEach Operations
|
|
739
|
+
forEachNode: (callback) => {
|
|
740
|
+
this.rowNodes.forEach((node) => callback(node));
|
|
741
|
+
},
|
|
742
|
+
forEachNodeAfterFilter: (callback) => {
|
|
743
|
+
this.displayedRowNodes.forEach((node) => callback(node));
|
|
744
|
+
},
|
|
745
|
+
forEachNodeAfterFilterAndSort: (callback) => {
|
|
746
|
+
this.displayedRowNodes.forEach((node) => callback(node));
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
return api;
|
|
366
751
|
}
|
|
367
|
-
|
|
368
|
-
private applyTransaction(
|
|
752
|
+
|
|
753
|
+
private applyTransaction(
|
|
754
|
+
transaction: RowDataTransaction<TData>
|
|
755
|
+
): RowDataTransactionResult | null {
|
|
369
756
|
const result: RowDataTransactionResult = {
|
|
370
757
|
add: [],
|
|
371
758
|
update: [],
|
|
372
|
-
remove: []
|
|
759
|
+
remove: [],
|
|
373
760
|
};
|
|
374
|
-
|
|
761
|
+
|
|
375
762
|
let dataChanged = false;
|
|
376
763
|
|
|
377
764
|
if (transaction.add) {
|
|
378
765
|
transaction.add.forEach((data, index) => {
|
|
379
|
-
const
|
|
766
|
+
const _id = this.getRowId(data, this.rowData.length + index);
|
|
380
767
|
this.rowData.push(data);
|
|
381
768
|
dataChanged = true;
|
|
382
|
-
|
|
769
|
+
|
|
383
770
|
// We'll create the actual node during the pipeline re-run
|
|
384
771
|
// but we can return a placeholder result for now as AG Grid does
|
|
385
772
|
});
|
|
386
773
|
}
|
|
387
|
-
|
|
774
|
+
|
|
388
775
|
if (transaction.update) {
|
|
389
|
-
transaction.update.forEach(data => {
|
|
776
|
+
transaction.update.forEach((data) => {
|
|
390
777
|
const id = this.getRowId(data, 0);
|
|
391
|
-
const index = this.rowData.findIndex(r => this.getRowId(r, 0) === id);
|
|
778
|
+
const index = this.rowData.findIndex((r) => this.getRowId(r, 0) === id);
|
|
392
779
|
if (index !== -1) {
|
|
393
780
|
this.rowData[index] = data;
|
|
394
781
|
dataChanged = true;
|
|
395
|
-
|
|
782
|
+
|
|
396
783
|
const existingNode = this.rowNodes.get(id);
|
|
397
784
|
if (existingNode) {
|
|
398
785
|
existingNode.data = data;
|
|
@@ -403,16 +790,16 @@ export class GridService<TData = any> {
|
|
|
403
790
|
}
|
|
404
791
|
|
|
405
792
|
if (transaction.remove) {
|
|
406
|
-
transaction.remove.forEach(data => {
|
|
793
|
+
transaction.remove.forEach((data) => {
|
|
407
794
|
const anyData = data as any;
|
|
408
|
-
const
|
|
795
|
+
const _dataId = anyData?.id;
|
|
409
796
|
const id = this.getRowId(data, 0);
|
|
410
|
-
|
|
411
|
-
const index = this.rowData.findIndex(r => this.getRowId(r, 0) === id);
|
|
797
|
+
|
|
798
|
+
const index = this.rowData.findIndex((r) => this.getRowId(r, 0) === id);
|
|
412
799
|
if (index !== -1) {
|
|
413
|
-
const
|
|
800
|
+
const _removedData = this.rowData.splice(index, 1)[0];
|
|
414
801
|
dataChanged = true;
|
|
415
|
-
|
|
802
|
+
|
|
416
803
|
const node = this.rowNodes.get(id);
|
|
417
804
|
if (node) {
|
|
418
805
|
this.rowNodes.delete(id);
|
|
@@ -426,10 +813,10 @@ export class GridService<TData = any> {
|
|
|
426
813
|
this.groupingDirty = true;
|
|
427
814
|
this.applySorting();
|
|
428
815
|
this.applyFiltering();
|
|
429
|
-
|
|
816
|
+
|
|
430
817
|
// Populate result.add after pipeline has run so we have the nodes
|
|
431
818
|
if (transaction.add) {
|
|
432
|
-
transaction.add.forEach(data => {
|
|
819
|
+
transaction.add.forEach((data) => {
|
|
433
820
|
const id = this.getRowId(data, 0);
|
|
434
821
|
const node = this.rowNodes.get(id);
|
|
435
822
|
if (node) result.add.push(node);
|
|
@@ -441,7 +828,7 @@ export class GridService<TData = any> {
|
|
|
441
828
|
|
|
442
829
|
return result;
|
|
443
830
|
}
|
|
444
|
-
|
|
831
|
+
|
|
445
832
|
private applySorting(): void {
|
|
446
833
|
this.groupingDirty = true;
|
|
447
834
|
if (this.sortModel.length === 0) {
|
|
@@ -479,8 +866,8 @@ export class GridService<TData = any> {
|
|
|
479
866
|
this.filteredRowData = [...this.rowData];
|
|
480
867
|
} else {
|
|
481
868
|
// Apply filters with AND logic
|
|
482
|
-
this.filteredRowData = this.rowData.filter(row => {
|
|
483
|
-
return Object.keys(this.filterModel).every(colId => {
|
|
869
|
+
this.filteredRowData = this.rowData.filter((row) => {
|
|
870
|
+
return Object.keys(this.filterModel).every((colId) => {
|
|
484
871
|
const filterItem = this.filterModel[colId];
|
|
485
872
|
if (!filterItem) return true;
|
|
486
873
|
|
|
@@ -502,7 +889,7 @@ export class GridService<TData = any> {
|
|
|
502
889
|
this.cumulativeRowHeights = [];
|
|
503
890
|
let currentTotal = 0;
|
|
504
891
|
|
|
505
|
-
this.displayedRowNodes.forEach(node => {
|
|
892
|
+
this.displayedRowNodes.forEach((node) => {
|
|
506
893
|
this.cumulativeRowHeights.push(currentTotal);
|
|
507
894
|
const height = node.rowHeight || defaultHeight;
|
|
508
895
|
currentTotal += height;
|
|
@@ -511,24 +898,30 @@ export class GridService<TData = any> {
|
|
|
511
898
|
this.totalHeight = currentTotal;
|
|
512
899
|
}
|
|
513
900
|
|
|
514
|
-
|
|
515
|
-
|
|
901
|
+
/**
|
|
902
|
+
* Get the Y position for a row index
|
|
903
|
+
*/
|
|
904
|
+
getRowY(index: number): number {
|
|
905
|
+
if (index <= 0) return 0;
|
|
906
|
+
if (index >= this.cumulativeRowHeights.length) return this.totalHeight;
|
|
516
907
|
return this.cumulativeRowHeights[index];
|
|
517
908
|
}
|
|
518
909
|
|
|
519
910
|
private getRowAtY(y: number): number {
|
|
520
911
|
if (this.cumulativeRowHeights.length === 0) return 0;
|
|
521
|
-
|
|
912
|
+
|
|
522
913
|
// Binary search for the row at position y
|
|
523
914
|
let low = 0;
|
|
524
915
|
let high = this.cumulativeRowHeights.length - 1;
|
|
525
|
-
|
|
916
|
+
|
|
526
917
|
while (low <= high) {
|
|
527
918
|
const mid = Math.floor((low + high) / 2);
|
|
528
919
|
const rowY = this.cumulativeRowHeights[mid];
|
|
529
|
-
const nextRowY =
|
|
530
|
-
|
|
531
|
-
|
|
920
|
+
const nextRowY =
|
|
921
|
+
mid < this.cumulativeRowHeights.length - 1
|
|
922
|
+
? this.cumulativeRowHeights[mid + 1]
|
|
923
|
+
: this.totalHeight;
|
|
924
|
+
|
|
532
925
|
if (y >= rowY && y < nextRowY) {
|
|
533
926
|
return mid;
|
|
534
927
|
} else if (y < rowY) {
|
|
@@ -537,7 +930,7 @@ export class GridService<TData = any> {
|
|
|
537
930
|
low = mid + 1;
|
|
538
931
|
}
|
|
539
932
|
}
|
|
540
|
-
|
|
933
|
+
|
|
541
934
|
return low >= this.cumulativeRowHeights.length ? this.cumulativeRowHeights.length - 1 : low;
|
|
542
935
|
}
|
|
543
936
|
|
|
@@ -547,9 +940,9 @@ export class GridService<TData = any> {
|
|
|
547
940
|
|
|
548
941
|
private getGroupColumns(): string[] {
|
|
549
942
|
if (!this.columnDefs) return [];
|
|
550
|
-
|
|
943
|
+
|
|
551
944
|
const groupCols: string[] = [];
|
|
552
|
-
this.columnDefs.forEach(def => {
|
|
945
|
+
this.columnDefs.forEach((def) => {
|
|
553
946
|
if ('rowGroup' in def && def.rowGroup === true && def.field) {
|
|
554
947
|
groupCols.push(def.field as string);
|
|
555
948
|
}
|
|
@@ -559,9 +952,9 @@ export class GridService<TData = any> {
|
|
|
559
952
|
|
|
560
953
|
private getPivotColumns(): string[] {
|
|
561
954
|
if (!this.columnDefs) return [];
|
|
562
|
-
|
|
955
|
+
|
|
563
956
|
const pivotCols: string[] = [];
|
|
564
|
-
this.columnDefs.forEach(def => {
|
|
957
|
+
this.columnDefs.forEach((def) => {
|
|
565
958
|
if ('pivot' in def && def.pivot === true && def.field) {
|
|
566
959
|
pivotCols.push(def.field as string);
|
|
567
960
|
}
|
|
@@ -571,9 +964,9 @@ export class GridService<TData = any> {
|
|
|
571
964
|
|
|
572
965
|
private getValueColumns(): ColDef<TData>[] {
|
|
573
966
|
if (!this.columnDefs) return [];
|
|
574
|
-
|
|
967
|
+
|
|
575
968
|
const valueCols: ColDef<TData>[] = [];
|
|
576
|
-
this.columnDefs.forEach(def => {
|
|
969
|
+
this.columnDefs.forEach((def) => {
|
|
577
970
|
if (!('children' in def) && def.aggFunc && def.field) {
|
|
578
971
|
valueCols.push(def);
|
|
579
972
|
}
|
|
@@ -584,7 +977,7 @@ export class GridService<TData = any> {
|
|
|
584
977
|
private generatePivotColumnDefs(): void {
|
|
585
978
|
const pivotColumns = this.getPivotColumns();
|
|
586
979
|
const valueColumns = this.getValueColumns();
|
|
587
|
-
|
|
980
|
+
|
|
588
981
|
if (pivotColumns.length === 0 || valueColumns.length === 0) {
|
|
589
982
|
this.pivotColumnDefs = null;
|
|
590
983
|
return;
|
|
@@ -592,8 +985,8 @@ export class GridService<TData = any> {
|
|
|
592
985
|
|
|
593
986
|
// 1. Find all unique pivot keys
|
|
594
987
|
const pivotKeys = new Set<string>();
|
|
595
|
-
this.filteredRowData.forEach(row => {
|
|
596
|
-
const key = pivotColumns.map(col => (row as any)[col]).join('_');
|
|
988
|
+
this.filteredRowData.forEach((row) => {
|
|
989
|
+
const key = pivotColumns.map((col) => (row as any)[col]).join('_');
|
|
597
990
|
pivotKeys.add(key);
|
|
598
991
|
});
|
|
599
992
|
|
|
@@ -602,8 +995,8 @@ export class GridService<TData = any> {
|
|
|
602
995
|
// 2. Generate column groups for each pivot key
|
|
603
996
|
const newPivotColDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
|
|
604
997
|
|
|
605
|
-
sortedPivotKeys.forEach(pivotKey => {
|
|
606
|
-
const children: ColDef<TData>[] = valueColumns.map(valCol => {
|
|
998
|
+
sortedPivotKeys.forEach((pivotKey) => {
|
|
999
|
+
const children: ColDef<TData>[] = valueColumns.map((valCol) => {
|
|
607
1000
|
const valueName = valCol.headerName || String(valCol.field);
|
|
608
1001
|
return {
|
|
609
1002
|
...valCol,
|
|
@@ -612,13 +1005,13 @@ export class GridService<TData = any> {
|
|
|
612
1005
|
// We use a custom field accessor for pivoted data
|
|
613
1006
|
field: `pivotData.${pivotKey}.${String(valCol.field)}` as any,
|
|
614
1007
|
pivot: false, // These are the results, not the pivot sources
|
|
615
|
-
rowGroup: false
|
|
1008
|
+
rowGroup: false,
|
|
616
1009
|
};
|
|
617
1010
|
});
|
|
618
1011
|
|
|
619
1012
|
newPivotColDefs.push({
|
|
620
1013
|
headerName: pivotKey,
|
|
621
|
-
children: children
|
|
1014
|
+
children: children,
|
|
622
1015
|
});
|
|
623
1016
|
});
|
|
624
1017
|
|
|
@@ -627,7 +1020,7 @@ export class GridService<TData = any> {
|
|
|
627
1020
|
|
|
628
1021
|
private applyGrouping(): void {
|
|
629
1022
|
const groupColumns = this.getGroupColumns();
|
|
630
|
-
|
|
1023
|
+
|
|
631
1024
|
if (this.isPivotMode) {
|
|
632
1025
|
this.generatePivotColumnDefs();
|
|
633
1026
|
// If we have pivot columns but weren't grouping, we need to initialize them
|
|
@@ -645,15 +1038,15 @@ export class GridService<TData = any> {
|
|
|
645
1038
|
// Only re-group if filters or data changed
|
|
646
1039
|
if (this.groupingDirty || !this.cachedGroupedData) {
|
|
647
1040
|
this.cachedGroupedData = this.groupByColumns(this.filteredRowData, groupColumns, 0);
|
|
648
|
-
|
|
1041
|
+
|
|
649
1042
|
if (this.isPivotMode) {
|
|
650
|
-
// Already called above, but we do it again after grouping to be sure
|
|
1043
|
+
// Already called above, but we do it again after grouping to be sure
|
|
651
1044
|
// (though in Pivot Mode we usually want grouping)
|
|
652
1045
|
this.generatePivotColumnDefs();
|
|
653
1046
|
this.initializeColumns(); // Re-initialize with new pivot columns
|
|
654
1047
|
this.gridStateChanged$.next({ type: 'columnsChanged' });
|
|
655
1048
|
}
|
|
656
|
-
|
|
1049
|
+
|
|
657
1050
|
this.groupingDirty = false;
|
|
658
1051
|
}
|
|
659
1052
|
|
|
@@ -687,7 +1080,7 @@ export class GridService<TData = any> {
|
|
|
687
1080
|
children: data,
|
|
688
1081
|
expanded: true,
|
|
689
1082
|
aggregation: this.calculateAggregations(data, 'Summary'),
|
|
690
|
-
pivotData: this.calculatePivotData(data)
|
|
1083
|
+
pivotData: this.calculatePivotData(data),
|
|
691
1084
|
};
|
|
692
1085
|
return [rootGroup];
|
|
693
1086
|
}
|
|
@@ -698,19 +1091,19 @@ export class GridService<TData = any> {
|
|
|
698
1091
|
const groups = new Map<any, TData[]>();
|
|
699
1092
|
|
|
700
1093
|
// Group data by the current field
|
|
701
|
-
data.forEach(item => {
|
|
1094
|
+
data.forEach((item) => {
|
|
702
1095
|
const key = (item as any)[groupField];
|
|
703
1096
|
if (!groups.has(key)) {
|
|
704
1097
|
groups.set(key, []);
|
|
705
1098
|
}
|
|
706
|
-
groups.get(key)
|
|
1099
|
+
groups.get(key)?.push(item);
|
|
707
1100
|
});
|
|
708
1101
|
|
|
709
1102
|
// Create group nodes
|
|
710
1103
|
const result: (TData | GroupRowNode<TData>)[] = [];
|
|
711
1104
|
groups.forEach((items, key) => {
|
|
712
1105
|
const children = this.groupByColumns(items, groupColumns, level + 1);
|
|
713
|
-
|
|
1106
|
+
|
|
714
1107
|
const groupNode: GroupRowNode<TData> = {
|
|
715
1108
|
id: `group-${groupField}-${key}-${level}`,
|
|
716
1109
|
groupKey: key,
|
|
@@ -719,16 +1112,16 @@ export class GridService<TData = any> {
|
|
|
719
1112
|
children,
|
|
720
1113
|
expanded: this.expandedGroups.has(`group-${groupField}-${key}-${level}`),
|
|
721
1114
|
aggregation: this.calculateAggregations(items, groupField),
|
|
722
|
-
pivotData: this.isPivotMode ? this.calculatePivotData(items) : undefined
|
|
1115
|
+
pivotData: this.isPivotMode ? this.calculatePivotData(items) : undefined,
|
|
723
1116
|
};
|
|
724
|
-
|
|
1117
|
+
|
|
725
1118
|
result.push(groupNode);
|
|
726
1119
|
});
|
|
727
1120
|
|
|
728
1121
|
return result;
|
|
729
1122
|
}
|
|
730
1123
|
|
|
731
|
-
private calculateAggregations(data: TData[],
|
|
1124
|
+
private calculateAggregations(data: TData[], _groupField: string): { [field: string]: any } {
|
|
732
1125
|
return this.calculateColumnAggregations(data);
|
|
733
1126
|
}
|
|
734
1127
|
|
|
@@ -737,12 +1130,12 @@ export class GridService<TData = any> {
|
|
|
737
1130
|
const pivotGroups = new Map<string, TData[]>();
|
|
738
1131
|
|
|
739
1132
|
// Sub-group by pivot columns within this row group
|
|
740
|
-
data.forEach(item => {
|
|
741
|
-
const key = pivotColumns.map(col => (item as any)[col]).join('_');
|
|
1133
|
+
data.forEach((item) => {
|
|
1134
|
+
const key = pivotColumns.map((col) => (item as any)[col]).join('_');
|
|
742
1135
|
if (!pivotGroups.has(key)) {
|
|
743
1136
|
pivotGroups.set(key, []);
|
|
744
1137
|
}
|
|
745
|
-
pivotGroups.get(key)
|
|
1138
|
+
pivotGroups.get(key)?.push(item);
|
|
746
1139
|
});
|
|
747
1140
|
|
|
748
1141
|
const pivotData: { [pivotKey: string]: { [field: string]: any } } = {};
|
|
@@ -755,18 +1148,20 @@ export class GridService<TData = any> {
|
|
|
755
1148
|
|
|
756
1149
|
public calculateColumnAggregations(data: TData[]): { [field: string]: any } {
|
|
757
1150
|
const aggregations: { [field: string]: any } = {};
|
|
758
|
-
|
|
1151
|
+
|
|
759
1152
|
if (!this.columnDefs) return aggregations;
|
|
760
1153
|
|
|
761
|
-
this.columnDefs.forEach(def => {
|
|
1154
|
+
this.columnDefs.forEach((def) => {
|
|
762
1155
|
// Skip column groups
|
|
763
1156
|
if ('children' in def) return;
|
|
764
|
-
|
|
1157
|
+
|
|
765
1158
|
if (!def.field || !def.aggFunc) return;
|
|
766
|
-
|
|
1159
|
+
|
|
767
1160
|
const field = def.field as string;
|
|
768
|
-
const values = data
|
|
769
|
-
|
|
1161
|
+
const values = data
|
|
1162
|
+
.map((item) => (item as any)[field])
|
|
1163
|
+
.filter((v) => v !== null && v !== undefined);
|
|
1164
|
+
|
|
770
1165
|
if (values.length === 0) return;
|
|
771
1166
|
|
|
772
1167
|
if (typeof def.aggFunc === 'function') {
|
|
@@ -779,13 +1174,14 @@ export class GridService<TData = any> {
|
|
|
779
1174
|
aggregations[field] = values.reduce((sum, v) => sum + (Number(v) || 0), 0);
|
|
780
1175
|
break;
|
|
781
1176
|
case 'avg':
|
|
782
|
-
aggregations[field] =
|
|
1177
|
+
aggregations[field] =
|
|
1178
|
+
values.reduce((sum, v) => sum + (Number(v) || 0), 0) / values.length;
|
|
783
1179
|
break;
|
|
784
1180
|
case 'min':
|
|
785
|
-
aggregations[field] = Math.min(...values.map(v => Number(v) || 0));
|
|
1181
|
+
aggregations[field] = Math.min(...values.map((v) => Number(v) || 0));
|
|
786
1182
|
break;
|
|
787
1183
|
case 'max':
|
|
788
|
-
aggregations[field] = Math.max(...values.map(v => Number(v) || 0));
|
|
1184
|
+
aggregations[field] = Math.max(...values.map((v) => Number(v) || 0));
|
|
789
1185
|
break;
|
|
790
1186
|
case 'count':
|
|
791
1187
|
aggregations[field] = values.length;
|
|
@@ -803,7 +1199,7 @@ export class GridService<TData = any> {
|
|
|
803
1199
|
// DO NOT CLEAR this.rowNodes - reuse existing nodes to preserve state
|
|
804
1200
|
this.displayedRowNodes = [];
|
|
805
1201
|
const flattened = this.flattenGroupedDataWithLevel(this.cachedGroupedData || []);
|
|
806
|
-
|
|
1202
|
+
|
|
807
1203
|
flattened.forEach((entry, index) => {
|
|
808
1204
|
const { item, level } = entry;
|
|
809
1205
|
let id: string;
|
|
@@ -815,11 +1211,11 @@ export class GridService<TData = any> {
|
|
|
815
1211
|
// Group node
|
|
816
1212
|
id = item.id;
|
|
817
1213
|
// Re-use aggregation data from the group node
|
|
818
|
-
data = {
|
|
1214
|
+
data = {
|
|
819
1215
|
...item.aggregation,
|
|
820
1216
|
pivotData: item.pivotData,
|
|
821
1217
|
[item.groupField]: item.groupKey,
|
|
822
|
-
'ag-Grid-AutoColumn': item.groupKey
|
|
1218
|
+
'ag-Grid-AutoColumn': item.groupKey,
|
|
823
1219
|
} as TData;
|
|
824
1220
|
isGroup = true;
|
|
825
1221
|
expanded = item.expanded;
|
|
@@ -856,11 +1252,30 @@ export class GridService<TData = any> {
|
|
|
856
1252
|
firstChild: index === 0,
|
|
857
1253
|
lastChild: index === flattened.length - 1,
|
|
858
1254
|
rowIndex: index,
|
|
859
|
-
displayedRowIndex: index
|
|
1255
|
+
displayedRowIndex: index,
|
|
1256
|
+
setSelected: (selected: boolean, clearSelection: boolean = false) => {
|
|
1257
|
+
const changed = node.selected !== selected || clearSelection;
|
|
1258
|
+
if (!changed) return;
|
|
1259
|
+
|
|
1260
|
+
if (clearSelection) {
|
|
1261
|
+
this.rowNodes.forEach((n) => {
|
|
1262
|
+
n.selected = false;
|
|
1263
|
+
});
|
|
1264
|
+
this.selectedRows.clear();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
node.selected = selected;
|
|
1268
|
+
if (selected) {
|
|
1269
|
+
this.selectedRows.add(node.id!);
|
|
1270
|
+
} else {
|
|
1271
|
+
this.selectedRows.delete(node.id!);
|
|
1272
|
+
}
|
|
1273
|
+
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
1274
|
+
},
|
|
860
1275
|
};
|
|
861
1276
|
this.rowNodes.set(id, node);
|
|
862
1277
|
}
|
|
863
|
-
|
|
1278
|
+
|
|
864
1279
|
this.displayedRowNodes.push(node);
|
|
865
1280
|
});
|
|
866
1281
|
|
|
@@ -874,11 +1289,11 @@ export class GridService<TData = any> {
|
|
|
874
1289
|
private flattenGroupedDataWithLevel(
|
|
875
1290
|
groupedData: (TData | GroupRowNode<TData>)[],
|
|
876
1291
|
level: number = 0,
|
|
877
|
-
result: { item: TData | GroupRowNode<TData
|
|
878
|
-
): { item: TData | GroupRowNode<TData
|
|
1292
|
+
result: { item: TData | GroupRowNode<TData>; level: number }[] = []
|
|
1293
|
+
): { item: TData | GroupRowNode<TData>; level: number }[] {
|
|
879
1294
|
for (const item of groupedData) {
|
|
880
1295
|
result.push({ item, level });
|
|
881
|
-
|
|
1296
|
+
|
|
882
1297
|
if (this.isGroupRowNode(item)) {
|
|
883
1298
|
if (item.expanded) {
|
|
884
1299
|
this.flattenGroupedDataWithLevel(item.children, level + 1, result);
|
|
@@ -904,11 +1319,43 @@ export class GridService<TData = any> {
|
|
|
904
1319
|
return this.matchesDateFilter(String(value), type, filter, filterTo);
|
|
905
1320
|
case 'boolean':
|
|
906
1321
|
return this.matchesBooleanFilter(value, filter);
|
|
1322
|
+
case 'set':
|
|
1323
|
+
return this.matchesSetFilter(value, filterItem);
|
|
907
1324
|
default:
|
|
908
1325
|
return true;
|
|
909
1326
|
}
|
|
910
1327
|
}
|
|
911
1328
|
|
|
1329
|
+
private matchesSetFilter(value: any, filterItem: FilterModelItem): boolean {
|
|
1330
|
+
// Set filter: value must be in the selected values array
|
|
1331
|
+
const filterValues = filterItem.values;
|
|
1332
|
+
if (!Array.isArray(filterValues) || filterValues.length === 0) {
|
|
1333
|
+
return true; // No filter applied
|
|
1334
|
+
}
|
|
1335
|
+
return filterValues.includes(value);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Get unique values for a column (for Set Filter)
|
|
1340
|
+
*/
|
|
1341
|
+
getUniqueValues(field: string): any[] {
|
|
1342
|
+
const values = new Set<any>();
|
|
1343
|
+
|
|
1344
|
+
this.rowData.forEach((data) => {
|
|
1345
|
+
const value = (data as any)[field];
|
|
1346
|
+
if (value !== null && value !== undefined) {
|
|
1347
|
+
values.add(value);
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
return Array.from(values).sort((a, b) => {
|
|
1352
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
1353
|
+
return a.localeCompare(b);
|
|
1354
|
+
}
|
|
1355
|
+
return String(a).localeCompare(String(b));
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
912
1359
|
private matchesTextFilter(value: string, type: string | undefined, filter: any): boolean {
|
|
913
1360
|
if (!type || filter === null || filter === undefined) {
|
|
914
1361
|
return true;
|
|
@@ -935,8 +1382,13 @@ export class GridService<TData = any> {
|
|
|
935
1382
|
}
|
|
936
1383
|
}
|
|
937
1384
|
|
|
938
|
-
private matchesNumberFilter(
|
|
939
|
-
|
|
1385
|
+
private matchesNumberFilter(
|
|
1386
|
+
value: number,
|
|
1387
|
+
type: string | undefined,
|
|
1388
|
+
filter: any,
|
|
1389
|
+
filterTo?: any
|
|
1390
|
+
): boolean {
|
|
1391
|
+
if (type === undefined || filter === null || filter === undefined || Number.isNaN(value)) {
|
|
940
1392
|
return true;
|
|
941
1393
|
}
|
|
942
1394
|
|
|
@@ -955,15 +1407,21 @@ export class GridService<TData = any> {
|
|
|
955
1407
|
return value < filterNum;
|
|
956
1408
|
case 'lessThanOrEqual':
|
|
957
1409
|
return value <= filterNum;
|
|
958
|
-
case 'inRange':
|
|
1410
|
+
case 'inRange': {
|
|
959
1411
|
const filterToNum = Number(filterTo);
|
|
960
1412
|
return value >= filterNum && value <= filterToNum;
|
|
1413
|
+
}
|
|
961
1414
|
default:
|
|
962
1415
|
return true;
|
|
963
1416
|
}
|
|
964
1417
|
}
|
|
965
1418
|
|
|
966
|
-
private matchesDateFilter(
|
|
1419
|
+
private matchesDateFilter(
|
|
1420
|
+
value: string,
|
|
1421
|
+
type: string | undefined,
|
|
1422
|
+
filter: any,
|
|
1423
|
+
filterTo?: any
|
|
1424
|
+
): boolean {
|
|
967
1425
|
if (!type || !filter) {
|
|
968
1426
|
return true;
|
|
969
1427
|
}
|
|
@@ -1005,16 +1463,16 @@ export class GridService<TData = any> {
|
|
|
1005
1463
|
this.groupingDirty = true;
|
|
1006
1464
|
// DO NOT CLEAR this.rowNodes - reuse existing nodes
|
|
1007
1465
|
this.displayedRowNodes = [];
|
|
1008
|
-
|
|
1466
|
+
|
|
1009
1467
|
// Separate rows by pinned state
|
|
1010
1468
|
const pinnedTopRows: TData[] = [];
|
|
1011
1469
|
const pinnedBottomRows: TData[] = [];
|
|
1012
1470
|
const normalRows: TData[] = [];
|
|
1013
|
-
|
|
1014
|
-
this.filteredRowData.forEach(data => {
|
|
1471
|
+
|
|
1472
|
+
this.filteredRowData.forEach((data) => {
|
|
1015
1473
|
const anyData = data as any;
|
|
1016
1474
|
const pinned = anyData?.pinned;
|
|
1017
|
-
|
|
1475
|
+
|
|
1018
1476
|
if (pinned === 'top') {
|
|
1019
1477
|
pinnedTopRows.push(data);
|
|
1020
1478
|
} else if (pinned === 'bottom') {
|
|
@@ -1023,15 +1481,16 @@ export class GridService<TData = any> {
|
|
|
1023
1481
|
normalRows.push(data);
|
|
1024
1482
|
}
|
|
1025
1483
|
});
|
|
1026
|
-
|
|
1484
|
+
|
|
1027
1485
|
const orderedRows = [...pinnedTopRows, ...normalRows, ...pinnedBottomRows];
|
|
1028
1486
|
|
|
1029
1487
|
orderedRows.forEach((data, index) => {
|
|
1030
1488
|
const id = this.getRowId(data, index);
|
|
1031
1489
|
const anyData = data as any;
|
|
1032
1490
|
const rowPinned = anyData?.pinned || false;
|
|
1033
|
-
const isMaster =
|
|
1034
|
-
|
|
1491
|
+
const isMaster =
|
|
1492
|
+
this.gridOptions?.masterDetail &&
|
|
1493
|
+
(this.gridOptions.isRowMaster ? this.gridOptions.isRowMaster(data) : true);
|
|
1035
1494
|
|
|
1036
1495
|
let node = this.rowNodes.get(id);
|
|
1037
1496
|
if (node) {
|
|
@@ -1058,11 +1517,30 @@ export class GridService<TData = any> {
|
|
|
1058
1517
|
firstChild: index === 0,
|
|
1059
1518
|
lastChild: index === orderedRows.length - 1,
|
|
1060
1519
|
rowIndex: index,
|
|
1061
|
-
displayedRowIndex: this.displayedRowNodes.length
|
|
1520
|
+
displayedRowIndex: this.displayedRowNodes.length,
|
|
1521
|
+
setSelected: (selected: boolean, clearSelection: boolean = false) => {
|
|
1522
|
+
const changed = node.selected !== selected || clearSelection;
|
|
1523
|
+
if (!changed) return;
|
|
1524
|
+
|
|
1525
|
+
if (clearSelection) {
|
|
1526
|
+
this.rowNodes.forEach((n) => {
|
|
1527
|
+
n.selected = false;
|
|
1528
|
+
});
|
|
1529
|
+
this.selectedRows.clear();
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
node.selected = selected;
|
|
1533
|
+
if (selected) {
|
|
1534
|
+
this.selectedRows.add(node.id!);
|
|
1535
|
+
} else {
|
|
1536
|
+
this.selectedRows.delete(node.id!);
|
|
1537
|
+
}
|
|
1538
|
+
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
1539
|
+
},
|
|
1062
1540
|
};
|
|
1063
1541
|
this.rowNodes.set(id!, node);
|
|
1064
1542
|
}
|
|
1065
|
-
|
|
1543
|
+
|
|
1066
1544
|
this.displayedRowNodes.push(node);
|
|
1067
1545
|
|
|
1068
1546
|
// If master row is expanded, insert a detail node
|
|
@@ -1085,7 +1563,11 @@ export class GridService<TData = any> {
|
|
|
1085
1563
|
firstChild: false,
|
|
1086
1564
|
lastChild: false,
|
|
1087
1565
|
rowIndex: null,
|
|
1088
|
-
displayedRowIndex: this.displayedRowNodes.length
|
|
1566
|
+
displayedRowIndex: this.displayedRowNodes.length,
|
|
1567
|
+
setSelected: (selected: boolean) => {
|
|
1568
|
+
detailNode!.selected = selected;
|
|
1569
|
+
this.gridStateChanged$.next({ type: 'selectionChanged' });
|
|
1570
|
+
},
|
|
1089
1571
|
};
|
|
1090
1572
|
this.rowNodes.set(detailId, detailNode);
|
|
1091
1573
|
} else {
|
|
@@ -1097,7 +1579,7 @@ export class GridService<TData = any> {
|
|
|
1097
1579
|
|
|
1098
1580
|
this.updateRowHeightCache();
|
|
1099
1581
|
}
|
|
1100
|
-
|
|
1582
|
+
|
|
1101
1583
|
private compareValues(a: any, b: any): number {
|
|
1102
1584
|
if (a === b) return 0;
|
|
1103
1585
|
if (a === null || a === undefined) return 1;
|
|
@@ -1107,10 +1589,10 @@ export class GridService<TData = any> {
|
|
|
1107
1589
|
}
|
|
1108
1590
|
return String(a).localeCompare(String(b));
|
|
1109
1591
|
}
|
|
1110
|
-
|
|
1592
|
+
|
|
1111
1593
|
private getGridState(): GridState {
|
|
1112
1594
|
const filterState: { [key: string]: FilterModelItem } = {};
|
|
1113
|
-
Object.keys(this.filterModel).forEach(key => {
|
|
1595
|
+
Object.keys(this.filterModel).forEach((key) => {
|
|
1114
1596
|
const item = this.filterModel[key];
|
|
1115
1597
|
if (item) {
|
|
1116
1598
|
filterState[key] = item;
|
|
@@ -1119,24 +1601,24 @@ export class GridService<TData = any> {
|
|
|
1119
1601
|
|
|
1120
1602
|
// Get pinned columns
|
|
1121
1603
|
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);
|
|
1604
|
+
const leftPinned = allColumns.filter((c) => c.pinned === 'left').map((c) => c.colId);
|
|
1605
|
+
const rightPinned = allColumns.filter((c) => c.pinned === 'right').map((c) => c.colId);
|
|
1124
1606
|
|
|
1125
1607
|
return {
|
|
1126
1608
|
sort: { sortModel: [...this.sortModel] },
|
|
1127
1609
|
filter: filterState,
|
|
1128
1610
|
columnPinning: { left: leftPinned, right: rightPinned },
|
|
1129
|
-
columnOrder: allColumns.map(col => ({
|
|
1611
|
+
columnOrder: allColumns.map((col) => ({
|
|
1130
1612
|
colId: col.colId,
|
|
1131
1613
|
width: col.width,
|
|
1132
1614
|
hide: !col.visible,
|
|
1133
1615
|
pinned: col.pinned,
|
|
1134
1616
|
sort: col.sort,
|
|
1135
|
-
sortIndex: col.sortIndex
|
|
1136
|
-
}))
|
|
1617
|
+
sortIndex: col.sortIndex,
|
|
1618
|
+
})),
|
|
1137
1619
|
};
|
|
1138
1620
|
}
|
|
1139
|
-
|
|
1621
|
+
|
|
1140
1622
|
private applyGridState(state: GridState): void {
|
|
1141
1623
|
if (state.sort) {
|
|
1142
1624
|
this.sortModel = state.sort.sortModel;
|
|
@@ -1146,7 +1628,7 @@ export class GridService<TData = any> {
|
|
|
1146
1628
|
this.filterModel = state.filter;
|
|
1147
1629
|
}
|
|
1148
1630
|
if (state.columnOrder) {
|
|
1149
|
-
state.columnOrder.forEach(colState => {
|
|
1631
|
+
state.columnOrder.forEach((colState) => {
|
|
1150
1632
|
const column = this.columns.get(colState.colId);
|
|
1151
1633
|
if (column) {
|
|
1152
1634
|
column.width = colState.width;
|
|
@@ -1158,42 +1640,42 @@ export class GridService<TData = any> {
|
|
|
1158
1640
|
});
|
|
1159
1641
|
}
|
|
1160
1642
|
}
|
|
1161
|
-
|
|
1162
|
-
private exportAsCsv(params?: CsvExportParams): void {
|
|
1643
|
+
|
|
1644
|
+
private exportAsCsv(params?: CsvExportParams, api?: any): void {
|
|
1163
1645
|
const fileName = params?.fileName || 'export.csv';
|
|
1164
1646
|
const delimiter = params?.delimiter || ',';
|
|
1165
1647
|
const skipHeader = params?.skipHeader || false;
|
|
1166
1648
|
const columnKeys = params?.columnKeys;
|
|
1167
1649
|
|
|
1168
1650
|
// Get columns to export
|
|
1169
|
-
let columnsToExport = this.getAllColumns().filter(col => col.visible);
|
|
1651
|
+
let columnsToExport = this.getAllColumns().filter((col) => col.visible);
|
|
1170
1652
|
if (columnKeys && columnKeys.length > 0) {
|
|
1171
|
-
columnsToExport = columnsToExport.filter(col => columnKeys.includes(col.colId));
|
|
1653
|
+
columnsToExport = columnsToExport.filter((col) => columnKeys.includes(col.colId));
|
|
1172
1654
|
}
|
|
1173
1655
|
|
|
1174
1656
|
// Build headers
|
|
1175
|
-
const headers = columnsToExport.map(col => {
|
|
1657
|
+
const headers = columnsToExport.map((col) => {
|
|
1176
1658
|
const headerName = col.headerName || col.colId;
|
|
1177
1659
|
// Escape quotes and wrap in quotes if contains delimiter
|
|
1178
1660
|
if (headerName.includes(delimiter) || headerName.includes('"') || headerName.includes('\n')) {
|
|
1179
|
-
return
|
|
1661
|
+
return `"${headerName.replace(/"/g, '""')}"`;
|
|
1180
1662
|
}
|
|
1181
1663
|
return headerName;
|
|
1182
1664
|
});
|
|
1183
1665
|
|
|
1184
1666
|
// Build rows
|
|
1185
|
-
const rows = this.rowData.map(data => {
|
|
1186
|
-
return columnsToExport.map(col => {
|
|
1667
|
+
const rows = this.rowData.map((data) => {
|
|
1668
|
+
return columnsToExport.map((col) => {
|
|
1187
1669
|
const value = (data as any)[col.field!];
|
|
1188
1670
|
let cellValue = '';
|
|
1189
|
-
|
|
1671
|
+
|
|
1190
1672
|
if (value !== null && value !== undefined) {
|
|
1191
1673
|
cellValue = String(value);
|
|
1192
1674
|
}
|
|
1193
|
-
|
|
1675
|
+
|
|
1194
1676
|
// Escape quotes and wrap in quotes if contains special chars
|
|
1195
1677
|
if (cellValue.includes(delimiter) || cellValue.includes('"') || cellValue.includes('\n')) {
|
|
1196
|
-
return
|
|
1678
|
+
return `"${cellValue.replace(/"/g, '""')}"`;
|
|
1197
1679
|
}
|
|
1198
1680
|
return cellValue;
|
|
1199
1681
|
});
|
|
@@ -1202,12 +1684,16 @@ export class GridService<TData = any> {
|
|
|
1202
1684
|
// Build CSV content
|
|
1203
1685
|
let csvContent = '';
|
|
1204
1686
|
if (!skipHeader) {
|
|
1205
|
-
csvContent += headers.join(delimiter)
|
|
1687
|
+
csvContent += `${headers.join(delimiter)}\n`;
|
|
1206
1688
|
}
|
|
1207
|
-
csvContent += rows.map(row => row.join(delimiter)).join('\n');
|
|
1689
|
+
csvContent += rows.map((row) => row.join(delimiter)).join('\n');
|
|
1208
1690
|
|
|
1209
1691
|
// Download CSV
|
|
1210
|
-
|
|
1692
|
+
if (api) {
|
|
1693
|
+
api.downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
|
|
1694
|
+
} else {
|
|
1695
|
+
this.downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
|
|
1696
|
+
}
|
|
1211
1697
|
}
|
|
1212
1698
|
|
|
1213
1699
|
private exportAsExcel(params?: ExcelExportParams): void {
|
|
@@ -1216,9 +1702,9 @@ export class GridService<TData = any> {
|
|
|
1216
1702
|
const skipHeader = params?.skipHeader || false;
|
|
1217
1703
|
|
|
1218
1704
|
// Get columns to export
|
|
1219
|
-
let columnsToExport = this.getAllColumns().filter(col => col.visible);
|
|
1705
|
+
let columnsToExport = this.getAllColumns().filter((col) => col.visible);
|
|
1220
1706
|
if (params?.columnKeys && params.columnKeys.length > 0) {
|
|
1221
|
-
columnsToExport = columnsToExport.filter(col => params.columnKeys
|
|
1707
|
+
columnsToExport = columnsToExport.filter((col) => params.columnKeys?.includes(col.colId));
|
|
1222
1708
|
}
|
|
1223
1709
|
|
|
1224
1710
|
const workbook = new Workbook();
|
|
@@ -1226,18 +1712,18 @@ export class GridService<TData = any> {
|
|
|
1226
1712
|
|
|
1227
1713
|
// Add headers
|
|
1228
1714
|
if (!skipHeader) {
|
|
1229
|
-
const headerRow = worksheet.addRow(columnsToExport.map(col => col.headerName || col.colId));
|
|
1715
|
+
const headerRow = worksheet.addRow(columnsToExport.map((col) => col.headerName || col.colId));
|
|
1230
1716
|
headerRow.font = { bold: true };
|
|
1231
1717
|
headerRow.fill = {
|
|
1232
1718
|
type: 'pattern',
|
|
1233
1719
|
pattern: 'solid',
|
|
1234
|
-
fgColor: { argb: 'FFF0F0F0' }
|
|
1720
|
+
fgColor: { argb: 'FFF0F0F0' },
|
|
1235
1721
|
};
|
|
1236
1722
|
}
|
|
1237
1723
|
|
|
1238
1724
|
// Add data
|
|
1239
|
-
this.rowData.forEach(data => {
|
|
1240
|
-
const rowValues = columnsToExport.map(col => {
|
|
1725
|
+
this.rowData.forEach((data) => {
|
|
1726
|
+
const rowValues = columnsToExport.map((col) => {
|
|
1241
1727
|
const value = (data as any)[col.field!];
|
|
1242
1728
|
return value !== null && value !== undefined ? value : '';
|
|
1243
1729
|
});
|
|
@@ -1245,9 +1731,9 @@ export class GridService<TData = any> {
|
|
|
1245
1731
|
});
|
|
1246
1732
|
|
|
1247
1733
|
// Auto-fit columns (basic implementation)
|
|
1248
|
-
worksheet.columns.forEach((column,
|
|
1734
|
+
worksheet.columns.forEach((column, _i) => {
|
|
1249
1735
|
let maxLength = 0;
|
|
1250
|
-
column.eachCell
|
|
1736
|
+
column.eachCell?.({ includeEmpty: true }, (cell) => {
|
|
1251
1737
|
const columnLength = cell.value ? cell.value.toString().length : 10;
|
|
1252
1738
|
if (columnLength > maxLength) {
|
|
1253
1739
|
maxLength = columnLength;
|
|
@@ -1257,8 +1743,10 @@ export class GridService<TData = any> {
|
|
|
1257
1743
|
});
|
|
1258
1744
|
|
|
1259
1745
|
// Generate buffer and download
|
|
1260
|
-
workbook.xlsx.writeBuffer().then(buffer => {
|
|
1261
|
-
const blob = new Blob([buffer], {
|
|
1746
|
+
workbook.xlsx.writeBuffer().then((buffer) => {
|
|
1747
|
+
const blob = new Blob([buffer], {
|
|
1748
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1749
|
+
});
|
|
1262
1750
|
const url = URL.createObjectURL(blob);
|
|
1263
1751
|
const link = document.createElement('a');
|
|
1264
1752
|
link.href = url;
|
|
@@ -1267,11 +1755,11 @@ export class GridService<TData = any> {
|
|
|
1267
1755
|
URL.revokeObjectURL(url);
|
|
1268
1756
|
});
|
|
1269
1757
|
}
|
|
1270
|
-
|
|
1758
|
+
|
|
1271
1759
|
private getAllColumns(): Column[] {
|
|
1272
1760
|
return Array.from(this.columns.values());
|
|
1273
1761
|
}
|
|
1274
|
-
|
|
1762
|
+
|
|
1275
1763
|
private downloadFile(content: string, fileName: string, mimeType: string): void {
|
|
1276
1764
|
const blob = new Blob([content], { type: mimeType });
|
|
1277
1765
|
const url = URL.createObjectURL(blob);
|
|
@@ -1281,4 +1769,148 @@ export class GridService<TData = any> {
|
|
|
1281
1769
|
link.click();
|
|
1282
1770
|
URL.revokeObjectURL(url);
|
|
1283
1771
|
}
|
|
1772
|
+
|
|
1773
|
+
// ============================================================================
|
|
1774
|
+
// STATE PERSISTENCE API
|
|
1775
|
+
// ============================================================================
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
* Get current grid state
|
|
1779
|
+
* Includes: columns (order, size, visibility, pinning), filters, sort, grouping
|
|
1780
|
+
*/
|
|
1781
|
+
getState(): GridState {
|
|
1782
|
+
const columns = this.getAllColumns();
|
|
1783
|
+
|
|
1784
|
+
return {
|
|
1785
|
+
columnOrder: columns.map((col) => ({
|
|
1786
|
+
colId: col.colId,
|
|
1787
|
+
width: col.width,
|
|
1788
|
+
hide: !col.visible,
|
|
1789
|
+
pinned: col.pinned,
|
|
1790
|
+
sort: col.sort,
|
|
1791
|
+
sortIndex: col.sortIndex,
|
|
1792
|
+
aggFunc: col.aggFunc,
|
|
1793
|
+
})),
|
|
1794
|
+
filter: { ...this.filterModel },
|
|
1795
|
+
sort: { sortModel: [...this.sortModel] },
|
|
1796
|
+
rowGrouping: {
|
|
1797
|
+
rowGroupCols: this.getGroupColumns(),
|
|
1798
|
+
valueCols: [],
|
|
1799
|
+
pivotCols: [],
|
|
1800
|
+
isPivotMode: false,
|
|
1801
|
+
},
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Restore grid state
|
|
1807
|
+
* Applies column state, filters, sort, and grouping
|
|
1808
|
+
*/
|
|
1809
|
+
setState(state: GridState): void {
|
|
1810
|
+
// Restore column state
|
|
1811
|
+
if (state.columnOrder) {
|
|
1812
|
+
const newColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
|
|
1813
|
+
|
|
1814
|
+
state.columnOrder.forEach((colState) => {
|
|
1815
|
+
const colDef = this.columnDefs?.find((d) =>
|
|
1816
|
+
'children' in d
|
|
1817
|
+
? false
|
|
1818
|
+
: (d as ColDef<TData>).colId === colState.colId ||
|
|
1819
|
+
(d as ColDef<TData>).field === colState.colId
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
if (colDef && !('children' in colDef)) {
|
|
1823
|
+
newColumnDefs.push({
|
|
1824
|
+
...colDef,
|
|
1825
|
+
width: colState.width,
|
|
1826
|
+
hide: colState.hide,
|
|
1827
|
+
pinned: colState.pinned || false,
|
|
1828
|
+
sort: colState.sort,
|
|
1829
|
+
sortIndex: colState.sortIndex,
|
|
1830
|
+
rowGroup: colState.rowGroupIndex !== undefined,
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
if (newColumnDefs.length > 0) {
|
|
1836
|
+
this.columnDefs = newColumnDefs;
|
|
1837
|
+
this.initializeColumns();
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// Restore filters
|
|
1842
|
+
if (state.filter) {
|
|
1843
|
+
this.filterModel = { ...state.filter };
|
|
1844
|
+
this.applyFiltering();
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Restore sort
|
|
1848
|
+
if (state.sort?.sortModel && state.sort.sortModel.length > 0) {
|
|
1849
|
+
this.sortModel = [...state.sort.sortModel];
|
|
1850
|
+
this.applyFiltering(); // This will trigger sorting
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Restore row grouping (handled through column state)
|
|
1854
|
+
// Row grouping is applied when columns are restored with rowGroup: true
|
|
1855
|
+
|
|
1856
|
+
// Emit state change event
|
|
1857
|
+
this.gridStateChanged$.next({ type: 'state-restored' });
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Save grid state to LocalStorage
|
|
1862
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
1863
|
+
*/
|
|
1864
|
+
saveState(key: string = 'argent-grid-state'): void {
|
|
1865
|
+
try {
|
|
1866
|
+
const state = this.getState();
|
|
1867
|
+
localStorage.setItem(key, JSON.stringify(state));
|
|
1868
|
+
this.gridStateChanged$.next({ type: 'state-saved', key });
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
console.warn('Failed to save grid state:', error);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
/**
|
|
1875
|
+
* Restore grid state from LocalStorage
|
|
1876
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
1877
|
+
* @returns true if state was restored, false if no state found
|
|
1878
|
+
*/
|
|
1879
|
+
restoreState(key: string = 'argent-grid-state'): boolean {
|
|
1880
|
+
try {
|
|
1881
|
+
const stateJson = localStorage.getItem(key);
|
|
1882
|
+
if (!stateJson) {
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const state: GridState = JSON.parse(stateJson);
|
|
1887
|
+
this.setState(state);
|
|
1888
|
+
this.gridStateChanged$.next({ type: 'state-restored', key });
|
|
1889
|
+
return true;
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
console.warn('Failed to restore grid state:', error);
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Clear grid state from LocalStorage
|
|
1898
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
1899
|
+
*/
|
|
1900
|
+
clearState(key: string = 'argent-grid-state'): void {
|
|
1901
|
+
try {
|
|
1902
|
+
localStorage.removeItem(key);
|
|
1903
|
+
this.gridStateChanged$.next({ type: 'state-cleared', key });
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
console.warn('Failed to clear grid state:', error);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
/**
|
|
1910
|
+
* Check if state exists in LocalStorage
|
|
1911
|
+
* @param key - Storage key (default: 'argent-grid-state')
|
|
1912
|
+
*/
|
|
1913
|
+
hasState(key: string = 'argent-grid-state'): boolean {
|
|
1914
|
+
return localStorage.getItem(key) !== null;
|
|
1915
|
+
}
|
|
1284
1916
|
}
|