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.
Files changed (122) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.github/workflows/pages.yml +6 -12
  3. package/.storybook/main.ts +20 -0
  4. package/.storybook/preview.ts +18 -0
  5. package/.storybook/tsconfig.json +24 -0
  6. package/AGENTS.md +70 -27
  7. package/README.md +51 -34
  8. package/angular.json +66 -0
  9. package/biome.json +66 -0
  10. package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
  11. package/docs/AG-GRID-COMPARISON.md +725 -0
  12. package/docs/CELL-RENDERER-GUIDE.md +241 -0
  13. package/docs/CONTEXT-MENU-GUIDE.md +371 -0
  14. package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
  15. package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
  16. package/docs/PERFORMANCE-REVIEW.md +571 -0
  17. package/docs/RESEARCH-STATUS.md +234 -0
  18. package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
  19. package/docs/STORYBOOK-REFACTOR.md +215 -0
  20. package/docs/STORYBOOK-STATUS.md +156 -0
  21. package/docs/TEST-COVERAGE-REPORT.md +276 -0
  22. package/docs/THEME-API-GUIDE.md +445 -0
  23. package/docs/THEME-API-PLAN.md +364 -0
  24. package/e2e/advanced.spec.ts +109 -0
  25. package/e2e/argentgrid.spec.ts +65 -0
  26. package/e2e/benchmark.spec.ts +52 -0
  27. package/e2e/cell-renderers.spec.ts +152 -0
  28. package/e2e/debug-streaming.spec.ts +31 -0
  29. package/e2e/dnd.spec.ts +73 -0
  30. package/e2e/screenshots.spec.ts +52 -0
  31. package/e2e/theming.spec.ts +35 -0
  32. package/e2e/visual.spec.ts +112 -0
  33. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  37. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  38. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  39. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  40. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  41. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  42. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  43. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  44. package/package.json +21 -7
  45. package/plan.md +56 -28
  46. package/playwright.config.ts +38 -0
  47. package/setup-vitest.ts +10 -13
  48. package/src/lib/argent-grid.module.ts +10 -12
  49. package/src/lib/components/argent-grid.component.css +281 -321
  50. package/src/lib/components/argent-grid.component.html +295 -207
  51. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  52. package/src/lib/components/argent-grid.component.ts +1193 -290
  53. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  54. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  55. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  56. package/src/lib/components/set-filter/set-filter.component.ts +307 -0
  57. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  58. package/src/lib/directives/click-outside.directive.ts +19 -0
  59. package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
  60. package/src/lib/rendering/canvas-renderer.ts +456 -452
  61. package/src/lib/rendering/live-data-handler.ts +110 -0
  62. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  63. package/src/lib/rendering/render/blit.spec.ts +16 -27
  64. package/src/lib/rendering/render/blit.ts +48 -36
  65. package/src/lib/rendering/render/cells.spec.ts +132 -0
  66. package/src/lib/rendering/render/cells.ts +167 -28
  67. package/src/lib/rendering/render/column-utils.ts +95 -0
  68. package/src/lib/rendering/render/hit-test.ts +50 -0
  69. package/src/lib/rendering/render/index.ts +88 -76
  70. package/src/lib/rendering/render/lines.ts +53 -47
  71. package/src/lib/rendering/render/primitives.ts +423 -0
  72. package/src/lib/rendering/render/theme.spec.ts +8 -12
  73. package/src/lib/rendering/render/theme.ts +7 -10
  74. package/src/lib/rendering/render/types.ts +3 -2
  75. package/src/lib/rendering/render/walk.spec.ts +35 -38
  76. package/src/lib/rendering/render/walk.ts +94 -64
  77. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  78. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  79. package/src/lib/rendering/utils/index.ts +1 -1
  80. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  81. package/src/lib/services/grid.service.spec.ts +1241 -201
  82. package/src/lib/services/grid.service.ts +1204 -235
  83. package/src/lib/themes/parts/color-schemes.ts +132 -0
  84. package/src/lib/themes/parts/icon-sets.ts +258 -0
  85. package/src/lib/themes/theme-builder.ts +347 -0
  86. package/src/lib/themes/theme-quartz.ts +72 -0
  87. package/src/lib/themes/types.ts +238 -0
  88. package/src/lib/types/ag-grid-types.ts +573 -14
  89. package/src/public-api.ts +39 -9
  90. package/src/stories/Advanced.stories.ts +249 -0
  91. package/src/stories/ArgentGrid.stories.ts +301 -0
  92. package/src/stories/Benchmark.stories.ts +76 -0
  93. package/src/stories/CellRenderers.stories.ts +395 -0
  94. package/src/stories/Filtering.stories.ts +292 -0
  95. package/src/stories/Grouping.stories.ts +290 -0
  96. package/src/stories/Streaming.stories.ts +57 -0
  97. package/src/stories/Theming.stories.ts +137 -0
  98. package/src/stories/Tooltips.stories.ts +381 -0
  99. package/src/stories/benchmark-wrapper.component.ts +355 -0
  100. package/src/stories/story-utils.ts +88 -0
  101. package/src/stories/streaming-wrapper.component.ts +441 -0
  102. package/tsconfig.json +1 -0
  103. package/tsconfig.storybook.json +10 -0
  104. package/vitest.config.ts +9 -9
  105. package/demo-app/README.md +0 -70
  106. package/demo-app/angular.json +0 -78
  107. package/demo-app/e2e/benchmark.spec.ts +0 -53
  108. package/demo-app/e2e/demo-page.spec.ts +0 -77
  109. package/demo-app/e2e/grid-features.spec.ts +0 -269
  110. package/demo-app/package-lock.json +0 -14023
  111. package/demo-app/package.json +0 -36
  112. package/demo-app/playwright-test-menu.js +0 -19
  113. package/demo-app/playwright.config.ts +0 -23
  114. package/demo-app/src/app/app.component.ts +0 -10
  115. package/demo-app/src/app/app.config.ts +0 -13
  116. package/demo-app/src/app/app.routes.ts +0 -7
  117. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  118. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  119. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  120. package/demo-app/src/index.html +0 -19
  121. package/demo-app/src/main.ts +0 -6
  122. package/demo-app/tsconfig.json +0 -31
@@ -1,27 +1,33 @@
1
- import { Injectable, Inject, Optional } from '@angular/core';
2
- import { Subject } from 'rxjs';
1
+ import { moveItemInArray } from '@angular/cdk/drag-drop';
2
+ import { Injectable } from '@angular/core';
3
3
  import { Workbook } from 'exceljs';
4
- import { GridApi,
5
- GridOptions,
4
+ import { Subject } from 'rxjs';
5
+ import {
6
+ CellRange,
6
7
  ColDef,
7
8
  ColGroupDef,
8
9
  Column,
9
- IRowNode,
10
+ ColumnGroup,
11
+ CsvExportParams,
12
+ ExcelExportParams,
10
13
  FilterModel,
11
14
  FilterModelItem,
12
- SortModelItem,
15
+ GridApi,
16
+ GridOptions,
13
17
  GridState,
18
+ GroupRowNode,
19
+ IRowNode,
14
20
  RowDataTransaction,
15
21
  RowDataTransactionResult,
16
- CsvExportParams,
17
- ExcelExportParams,
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<{ type: string, key?: string, value?: any }>();
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 initializeColumns(): void {
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
- // 1. Handle Auto Group Column (for singleColumn display)
89
- if (isGrouping && (groupDisplayType === 'singleColumn' || !this.gridOptions?.groupDisplayType)) {
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
- // 2. Process regular columns
106
- const columnsToProcess = (this.isPivotMode && this.pivotColumnDefs) ?
107
- [...this.columnDefs, ...this.pivotColumnDefs] :
108
- this.columnDefs;
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
- columnsToProcess.forEach((def, index) => {
111
- if ('children' in def) {
112
- // Column group
113
- def.children.forEach((child, childIndex) => {
114
- this.addColumn(child, index * 100 + childIndex, isGrouping);
115
- });
116
- } else {
117
- this.addColumn(def, index, isGrouping);
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 normalizePinned(pinned: boolean | 'left' | 'right' | null | undefined): 'left' | 'right' | false {
123
- if (pinned === 'left' || pinned === true) return 'left';
124
- if (pinned === 'right') return 'right';
125
- return false;
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
- private addColumn(def: ColDef<TData>, index: number, isGrouping: boolean): void {
129
- const colId = def.colId || def.field?.toString() || `col-${index}`;
130
-
131
- // Auto-hide columns that are being grouped (AG Grid default)
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
- // Auto-hide columns that are being pivoted
138
- if (this.isPivotMode && def.pivot && visible) {
139
- visible = false;
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
- // Auto-hide value columns if in pivot mode (they appear under pivot keys)
143
- if (this.isPivotMode && def.aggFunc && visible && !colId.startsWith('pivot_')) {
144
- visible = false;
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
- // In pivot mode, hide columns that are not part of grouping or pivot results
148
- if (this.isPivotMode && visible && !def.rowGroup && !colId.startsWith('pivot_') && colId !== 'ag-Grid-AutoColumn') {
149
- visible = false;
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
- const column: Column = {
153
- colId,
154
- field: def.field?.toString(),
155
- headerName: def.headerName,
156
- width: def.width || 150,
157
- minWidth: def.minWidth,
158
- maxWidth: def.maxWidth,
159
- pinned: this.normalizePinned(def.pinned),
160
- visible: visible,
161
- sort: (typeof def.sort === 'object' && def.sort !== null) ? (def.sort as any).sort : def.sort || null,
162
- sortIndex: def.sortIndex ?? undefined,
163
- aggFunc: typeof def.aggFunc === 'string' ? def.aggFunc : null
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
- this.columns.set(colId, column);
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
- return {
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
- this.applySorting();
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
- cutToClipboard: () => {},
273
- pasteFromClipboard: () => {},
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(node => {
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(transaction: RowDataTransaction<TData>): RowDataTransactionResult | null {
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 id = this.getRowId(data, this.rowData.length + index);
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 dataId = anyData?.id;
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 removedData = this.rowData.splice(index, 1)[0];
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
- this.gridStateChanged$.next({ type: 'transactionApplied' });
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
- private getRowY(index: number): number {
515
- if (index < 0 || index >= this.cumulativeRowHeights.length) return 0;
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 = mid < this.cumulativeRowHeights.length - 1 ?
530
- this.cumulativeRowHeights[mid + 1] : this.totalHeight;
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.updateExpansionStateInCache(this.cachedGroupedData);
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 updateExpansionStateInCache(groupedData: (TData | GroupRowNode<TData>)[]): void {
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
- this.updateExpansionStateInCache(item.children);
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)!.push(item);
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[], groupField: string): { [field: string]: any } {
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)!.push(item);
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.map(item => (item as any)[field]).filter(v => v !== null && v !== undefined);
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] = values.reduce((sum, v) => sum + (Number(v) || 0), 0) / values.length;
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>, level: number }[] = []
878
- ): { item: TData | GroupRowNode<TData>, level: number }[] {
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(value: number, type: string | undefined, filter: any, filterTo?: any): boolean {
939
- if (type === undefined || filter === null || filter === undefined || isNaN(value)) {
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(value: string, type: string | undefined, filter: any, filterTo?: any): boolean {
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 = this.gridOptions?.masterDetail &&
1034
- (this.gridOptions.isRowMaster ? this.gridOptions.isRowMaster(data) : true);
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 '"' + headerName.replace(/"/g, '""') + '"';
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 '"' + cellValue.replace(/"/g, '""') + '"';
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) + '\n';
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
- this.downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
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!.includes(col.colId));
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, i) => {
2071
+ worksheet.columns.forEach((column, _i) => {
1249
2072
  let maxLength = 0;
1250
- column.eachCell!({ includeEmpty: true }, (cell) => {
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], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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
  }