argent-grid 0.2.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 (55) hide show
  1. package/AGENTS.md +70 -27
  2. package/e2e/advanced.spec.ts +1 -1
  3. package/e2e/benchmark.spec.ts +7 -7
  4. package/e2e/cell-renderers.spec.ts +152 -0
  5. package/e2e/debug-streaming.spec.ts +31 -0
  6. package/e2e/dnd.spec.ts +73 -0
  7. package/e2e/screenshots.spec.ts +1 -1
  8. package/e2e/visual.spec.ts +30 -9
  9. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  10. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  11. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  12. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  13. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  14. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  15. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  16. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  17. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  18. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  19. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  20. package/package.json +5 -5
  21. package/plan.md +30 -34
  22. package/src/lib/components/argent-grid.component.css +258 -549
  23. package/src/lib/components/argent-grid.component.html +272 -306
  24. package/src/lib/components/argent-grid.component.ts +585 -135
  25. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  26. package/src/lib/components/argent-grid.selection.spec.ts +2 -2
  27. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  28. package/src/lib/components/set-filter/set-filter.component.ts +7 -2
  29. package/src/lib/rendering/canvas-renderer.spec.ts +148 -1
  30. package/src/lib/rendering/canvas-renderer.ts +177 -286
  31. package/src/lib/rendering/render/cells.ts +122 -5
  32. package/src/lib/rendering/render/column-utils.ts +27 -5
  33. package/src/lib/rendering/render/hit-test.ts +6 -11
  34. package/src/lib/rendering/render/index.ts +15 -6
  35. package/src/lib/rendering/render/lines.ts +12 -6
  36. package/src/lib/rendering/render/primitives.ts +269 -7
  37. package/src/lib/rendering/render/types.ts +2 -1
  38. package/src/lib/rendering/render/walk.ts +39 -19
  39. package/src/lib/services/grid.service.spec.ts +76 -0
  40. package/src/lib/services/grid.service.ts +451 -114
  41. package/src/lib/themes/theme-quartz.ts +2 -2
  42. package/src/lib/types/ag-grid-types.ts +500 -0
  43. package/src/stories/Advanced.stories.ts +78 -17
  44. package/src/stories/ArgentGrid.stories.ts +50 -26
  45. package/src/stories/Benchmark.stories.ts +17 -15
  46. package/src/stories/CellRenderers.stories.ts +205 -31
  47. package/src/stories/Filtering.stories.ts +56 -16
  48. package/src/stories/Grouping.stories.ts +86 -13
  49. package/src/stories/Streaming.stories.ts +57 -0
  50. package/src/stories/Theming.stories.ts +23 -10
  51. package/src/stories/Tooltips.stories.ts +381 -0
  52. package/src/stories/benchmark-wrapper.component.ts +69 -29
  53. package/src/stories/story-utils.ts +88 -0
  54. package/src/stories/streaming-wrapper.component.ts +441 -0
  55. package/tsconfig.json +1 -0
@@ -1,3 +1,4 @@
1
+ import { moveItemInArray } from '@angular/cdk/drag-drop';
1
2
  import { Injectable } from '@angular/core';
2
3
  import { Workbook } from 'exceljs';
3
4
  import { Subject } from 'rxjs';
@@ -6,6 +7,7 @@ import {
6
7
  ColDef,
7
8
  ColGroupDef,
8
9
  Column,
10
+ ColumnGroup,
9
11
  CsvExportParams,
10
12
  ExcelExportParams,
11
13
  FilterModel,
@@ -17,12 +19,15 @@ import {
17
19
  IRowNode,
18
20
  RowDataTransaction,
19
21
  RowDataTransactionResult,
22
+ SortDirection,
20
23
  SortModelItem,
21
24
  } from '../types/ag-grid-types';
22
25
 
23
26
  @Injectable()
24
27
  export class GridService<TData = any> {
25
28
  private columns: Map<string, Column> = new Map();
29
+ private columnGroups: ColumnGroup[] = [];
30
+ private headerDepth = 1;
26
31
  private rowData: TData[] = [];
27
32
  private rowNodes: Map<string, IRowNode<TData>> = new Map();
28
33
  private displayedRowNodes: IRowNode<TData>[] = [];
@@ -35,7 +40,12 @@ export class GridService<TData = any> {
35
40
  private cellRanges: CellRange[] = [];
36
41
  private gridId: string = '';
37
42
  private gridOptions: GridOptions<TData> | null = null;
38
- 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
+ }>();
39
49
 
40
50
  // Row height cache
41
51
  private cumulativeRowHeights: number[] = [];
@@ -62,7 +72,7 @@ export class GridService<TData = any> {
62
72
  this.gridOptions = gridOptions ? { ...gridOptions } : {};
63
73
  this.isPivotMode = !!this.gridOptions.pivotMode;
64
74
 
65
- this.initializeColumns();
75
+ this.initializeColumns(true);
66
76
 
67
77
  // Trigger initial pipeline run
68
78
  this.applySorting();
@@ -76,6 +86,12 @@ export class GridService<TData = any> {
76
86
  }
77
87
 
78
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;
79
95
  if (this.gridOptions?.selectionColumnDef?.checkboxes) return true;
80
96
  if (!this.columnDefs) return false;
81
97
 
@@ -89,17 +105,22 @@ export class GridService<TData = any> {
89
105
  return check(this.columnDefs);
90
106
  }
91
107
 
92
- private initializeColumns(): void {
108
+ private initializeColumns(clearColumns: boolean = true): void {
93
109
  if (!this.columnDefs) {
94
110
  return;
95
111
  }
96
112
 
113
+ const existingColumns = clearColumns ? [] : Array.from(this.columns.values());
97
114
  this.columns.clear();
115
+ this.columnGroups = [];
116
+ this.headerDepth = 1;
98
117
 
99
118
  const groupColumns = this.getGroupColumns();
100
119
  const isGrouping = groupColumns.length > 0;
101
120
  const groupDisplayType = this.gridOptions?.groupDisplayType || 'singleColumn';
102
121
 
122
+ const topLevelColumns: (Column | ColumnGroup)[] = [];
123
+
103
124
  // 1. Handle Selection Column
104
125
  if (this.hasCheckboxSelection()) {
105
126
  const selectionCol: Column = {
@@ -112,8 +133,10 @@ export class GridService<TData = any> {
112
133
  sort: null,
113
134
  checkboxSelection: true,
114
135
  headerCheckboxSelection: true,
136
+ colIndex: 0,
115
137
  };
116
138
  this.columns.set(selectionCol.colId, selectionCol);
139
+ topLevelColumns.push(selectionCol);
117
140
  }
118
141
 
119
142
  // 2. Handle Auto Group Column (for singleColumn display)
@@ -132,89 +155,384 @@ export class GridService<TData = any> {
132
155
  pinned: this.normalizePinned(autoGroupDef.pinned || 'left'),
133
156
  visible: true,
134
157
  sort: null,
158
+ colIndex: this.columns.size,
135
159
  };
136
160
  this.columns.set(autoGroupCol.colId, autoGroupCol);
161
+ topLevelColumns.push(autoGroupCol);
137
162
  }
138
163
 
139
- // 2. Process regular columns
140
- const columnsToProcess =
141
- this.isPivotMode && this.pivotColumnDefs
142
- ? [...this.columnDefs, ...this.pivotColumnDefs]
143
- : this.columnDefs;
144
-
145
- columnsToProcess.forEach((def, index) => {
146
- if ('children' in def) {
147
- // Column group
148
- def.children.forEach((child, childIndex) => {
149
- // Merge defaultColDef for nested columns too
150
- const mergedChild = { ...this.gridOptions?.defaultColDef, ...child };
151
- this.addColumn(mergedChild, index * 100 + childIndex, isGrouping);
152
- });
153
- } else {
154
- const mergedDef = { ...this.gridOptions?.defaultColDef, ...def };
155
- this.addColumn(mergedDef, index, isGrouping);
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
+ };
189
+
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);
156
271
  }
157
272
  });
158
- }
159
273
 
160
- private normalizePinned(
161
- pinned: boolean | 'left' | 'right' | null | undefined
162
- ): 'left' | 'right' | false {
163
- if (pinned === 'left' || pinned === true) return 'left';
164
- if (pinned === 'right') return 'right';
165
- return false;
274
+ // 5. Calculate header depth
275
+ this.headerDepth = this.calculateHeaderDepth(topLevelColumns);
166
276
  }
167
277
 
168
- private addColumn(def: ColDef<TData>, index: number, isGrouping: boolean): void {
169
- const colId = def.colId || def.field?.toString() || `col-${index}`;
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;
290
+ }
170
291
 
171
- // Auto-hide columns that are being grouped (AG Grid default)
172
- let visible = !def.hide;
173
- if (isGrouping && def.rowGroup && visible && this.gridOptions?.groupHideOpenParents !== false) {
174
- visible = false;
292
+ public getHeaderRows(): (Column | ColumnGroup)[][] {
293
+ const rows: (Column | ColumnGroup)[][] = [];
294
+ for (let i = 0; i < this.headerDepth; i++) {
295
+ rows[i] = [];
175
296
  }
176
297
 
177
- // Auto-hide columns that are being pivoted
178
- if (this.isPivotMode && def.pivot && visible) {
179
- visible = false;
180
- }
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)[] = [];
181
308
 
182
- // Auto-hide value columns if in pivot mode (they appear under pivot keys)
183
- if (this.isPivotMode && def.aggFunc && visible && !colId.startsWith('pivot_')) {
184
- visible = false;
309
+ if (this.hasCheckboxSelection()) {
310
+ topItems.push(this.columns.get('ag-Grid-SelectionColumn')!);
185
311
  }
186
312
 
187
- // In pivot mode, hide columns that are not part of grouping or pivot results
313
+ const isGrouping = this.getGroupColumns().length > 0;
314
+ const groupDisplayType = this.gridOptions?.groupDisplayType || 'singleColumn';
188
315
  if (
189
- this.isPivotMode &&
190
- visible &&
191
- !def.rowGroup &&
192
- !colId.startsWith('pivot_') &&
193
- colId !== 'ag-Grid-AutoColumn'
316
+ isGrouping &&
317
+ (groupDisplayType === 'singleColumn' || !this.gridOptions?.groupDisplayType)
194
318
  ) {
195
- visible = false;
319
+ topItems.push(this.columns.get('ag-Grid-AutoColumn')!);
320
+ }
321
+
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 });
196
351
  }
352
+ }
353
+
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;
197
376
 
198
- const column: Column = {
199
- colId,
200
- field: def.field?.toString(),
201
- headerName: def.headerName,
202
- width: def.width || 150,
203
- minWidth: def.minWidth,
204
- maxWidth: def.maxWidth,
205
- pinned: this.normalizePinned(def.pinned),
206
- visible: visible,
207
- sort:
208
- typeof def.sort === 'object' && def.sort !== null
209
- ? (def.sort as any).sort
210
- : def.sort || null,
211
- sortIndex: def.sortIndex ?? undefined,
212
- aggFunc: typeof def.aggFunc === 'string' ? def.aggFunc : null,
213
- checkboxSelection: !!def.checkboxSelection,
214
- headerCheckboxSelection: !!def.headerCheckboxSelection,
215
- filter: def.filter,
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
+ });
216
405
  };
217
- 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' });
412
+ }
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;
218
536
  }
219
537
 
220
538
  private getRowId(data: TData, index: number): string {
@@ -237,7 +555,7 @@ export class GridService<TData = any> {
237
555
  setColumnDefs: (colDefs) => {
238
556
  this.columnDefs = colDefs;
239
557
  this.groupingDirty = true;
240
- this.initializeColumns();
558
+ this.initializeColumns(true);
241
559
  },
242
560
  getColumn: (key) => {
243
561
  const colId = typeof key === 'string' ? key : key.colId;
@@ -247,6 +565,12 @@ export class GridService<TData = any> {
247
565
  getDisplayedRowAtIndex: (index) => {
248
566
  return this.displayedRowNodes[index] || null;
249
567
  },
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
+ },
250
574
 
251
575
  // Row Data API
252
576
  getRowData: () => [...this.filteredRowData],
@@ -301,11 +625,7 @@ export class GridService<TData = any> {
301
625
  this.gridStateChanged$.next({ type: 'sortChanged' });
302
626
  },
303
627
  getSortModel: () => [...this.sortModel],
304
- onSortChanged: () => {
305
- this.applySorting();
306
- this.applyFiltering(); // Re-filter and re-group after sort
307
- this.gridStateChanged$.next({ type: 'sortChanged' });
308
- },
628
+ // onSortChanged handled below
309
629
 
310
630
  // Pagination API
311
631
  paginationGetPageSize: () => 100,
@@ -392,7 +712,13 @@ export class GridService<TData = any> {
392
712
  if (!this.gridOptions) {
393
713
  this.gridOptions = {} as GridOptions<TData>;
394
714
  }
715
+ if (this.gridOptions[key] === value) return;
395
716
  this.gridOptions[key] = value;
717
+
718
+ if (key === 'rowHeight' || key === 'detailRowHeight') {
719
+ this.updateRowHeightCache();
720
+ }
721
+
396
722
  this.gridStateChanged$.next({ type: 'optionChanged', key: key as string, value });
397
723
  },
398
724
 
@@ -447,40 +773,6 @@ export class GridService<TData = any> {
447
773
  },
448
774
 
449
775
  // 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
776
  autoSizeColumns: (colKeys) => {
485
777
  // Basic implementation - set reasonable widths
486
778
  colKeys.forEach((key) => {
@@ -566,12 +858,20 @@ export class GridService<TData = any> {
566
858
  getValueColumns: () => {
567
859
  return [];
568
860
  },
569
- getRowGroupColumns: () => {
570
- return this.getGroupColumns();
571
- },
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(),
572
866
  getGroupDisplayType: () => {
573
867
  return this.gridOptions?.groupDisplayType || 'singleColumn';
574
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(),
575
875
 
576
876
  // Tool Panels
577
877
  setSideBarVisible: (_visible) => {
@@ -760,12 +1060,16 @@ export class GridService<TData = any> {
760
1060
  };
761
1061
 
762
1062
  let dataChanged = false;
1063
+ // Track changed row indices for efficient rendering
1064
+ const changedRowIndices: number[] = [];
763
1065
 
764
1066
  if (transaction.add) {
1067
+ const startIndex = this.rowData.length;
765
1068
  transaction.add.forEach((data, index) => {
766
1069
  const _id = this.getRowId(data, this.rowData.length + index);
767
1070
  this.rowData.push(data);
768
1071
  dataChanged = true;
1072
+ changedRowIndices.push(startIndex + index);
769
1073
 
770
1074
  // We'll create the actual node during the pipeline re-run
771
1075
  // but we can return a placeholder result for now as AG Grid does
@@ -779,6 +1083,7 @@ export class GridService<TData = any> {
779
1083
  if (index !== -1) {
780
1084
  this.rowData[index] = data;
781
1085
  dataChanged = true;
1086
+ changedRowIndices.push(index);
782
1087
 
783
1088
  const existingNode = this.rowNodes.get(id);
784
1089
  if (existingNode) {
@@ -799,6 +1104,8 @@ export class GridService<TData = any> {
799
1104
  if (index !== -1) {
800
1105
  const _removedData = this.rowData.splice(index, 1)[0];
801
1106
  dataChanged = true;
1107
+ // For removes, we mark the row as changed but it's being deleted
1108
+ // The component will handle this appropriately
802
1109
 
803
1110
  const node = this.rowNodes.get(id);
804
1111
  if (node) {
@@ -823,7 +1130,11 @@ export class GridService<TData = any> {
823
1130
  });
824
1131
  }
825
1132
 
826
- 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
+ });
827
1138
  }
828
1139
 
829
1140
  return result;
@@ -1048,18 +1359,44 @@ export class GridService<TData = any> {
1048
1359
  }
1049
1360
 
1050
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
+ }
1051
1368
  }
1052
1369
 
1053
- // Re-initialize from cache (respects current expansion state)
1054
- this.updateExpansionStateInCache(this.cachedGroupedData);
1370
+ // Re-initialize from cache (respects current expansion state in this.expandedGroups)
1371
+ this.syncExpansionStateFromSet(this.cachedGroupedData);
1055
1372
  this.initializeRowNodesFromGroupedData();
1056
1373
  }
1057
1374
 
1058
- 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 {
1059
1394
  for (const item of groupedData) {
1060
1395
  if (this.isGroupRowNode(item)) {
1061
1396
  item.expanded = this.expandedGroups.has(item.id);
1062
- this.updateExpansionStateInCache(item.children);
1397
+ if (item.children) {
1398
+ this.syncExpansionStateFromSet(item.children);
1399
+ }
1063
1400
  }
1064
1401
  }
1065
1402
  }