argent-grid 0.1.0 → 0.2.0

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