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,25 +1,57 @@
1
- import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ElementRef, ViewChild, ChangeDetectionStrategy, AfterViewInit, OnChanges, SimpleChanges, ChangeDetectorRef, Inject } from '@angular/core';
2
1
  import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
3
- import { GridApi, GridOptions, ColDef, ColGroupDef, IRowNode, Column, CellRange, DefaultMenuItem, MenuItemDef, GetContextMenuItemsParams } from '../types/ag-grid-types';
2
+ import {
3
+ AfterViewInit,
4
+ ChangeDetectionStrategy,
5
+ ChangeDetectorRef,
6
+ Component,
7
+ ElementRef,
8
+ EventEmitter,
9
+ Inject,
10
+ Input,
11
+ OnChanges,
12
+ OnDestroy,
13
+ OnInit,
14
+ Output,
15
+ SimpleChanges,
16
+ ViewChild,
17
+ } from '@angular/core';
4
18
  import { Subject } from 'rxjs';
5
19
  import { takeUntil } from 'rxjs/operators';
6
- import { GridService } from '../services/grid.service';
7
20
  import { CanvasRenderer } from '../rendering/canvas-renderer';
21
+ import { GridService } from '../services/grid.service';
22
+ import { applyThemeCSSVariables, convertThemeToGridTheme } from '../themes/theme-builder';
23
+ import {
24
+ CellRange,
25
+ ColDef,
26
+ ColGroupDef,
27
+ Column,
28
+ DefaultMenuItem,
29
+ GetContextMenuItemsParams,
30
+ GridApi,
31
+ GridOptions,
32
+ IRowNode,
33
+ MenuItemDef,
34
+ RowSelectionOptions,
35
+ } from '../types/ag-grid-types';
8
36
 
9
37
  @Component({
10
38
  selector: 'argent-grid',
11
39
  templateUrl: './argent-grid.component.html',
12
40
  styleUrls: ['./argent-grid.component.css'],
13
- changeDetection: ChangeDetectionStrategy.OnPush
41
+ changeDetection: ChangeDetectionStrategy.OnPush,
14
42
  })
15
- export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, AfterViewInit, OnChanges {
43
+ export class ArgentGridComponent<TData = any>
44
+ implements OnInit, OnDestroy, AfterViewInit, OnChanges
45
+ {
16
46
  @Input() columnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
17
47
  @Input() rowData: TData[] | null = null;
18
48
  @Input() gridOptions: GridOptions<TData> | null = null;
49
+ @Input() theme: any;
19
50
  @Input() height = '500px';
20
51
  @Input() width = '100%';
21
52
  @Input() rowHeight = 32;
22
-
53
+ @Input() rowSelection: RowSelectionOptions | 'single' | 'multiple' | undefined;
54
+
23
55
  @Output() gridReady = new EventEmitter<GridApi<TData>>();
24
56
  @Output() rowClicked = new EventEmitter<{ data: TData; node: IRowNode<TData> }>();
25
57
  @Output() selectionChanged = new EventEmitter<IRowNode<TData>[]>();
@@ -41,8 +73,9 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
41
73
 
42
74
  get totalWidth(): number {
43
75
  if (!this.gridApi) return 0;
44
- return this.gridApi.getAllColumns()
45
- .filter(col => col.visible)
76
+ return this.gridApi
77
+ .getAllColumns()
78
+ .filter((col) => col.visible)
46
79
  .reduce((sum, col) => sum + col.width, 0);
47
80
  }
48
81
 
@@ -52,6 +85,14 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
52
85
  isAllSelected = false;
53
86
  isIndeterminateSelection = false;
54
87
 
88
+ hasCheckboxSelection(col: Column): boolean {
89
+ return col.colId === 'ag-Grid-SelectionColumn';
90
+ }
91
+
92
+ hasHeaderCheckbox(col: Column): boolean {
93
+ return col.colId === 'ag-Grid-SelectionColumn';
94
+ }
95
+
55
96
  trackByColumn(index: number, col: Column | ColDef<TData> | ColGroupDef<TData>): string {
56
97
  return (col as any).colId || (col as any).field?.toString() || index.toString();
57
98
  }
@@ -75,7 +116,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
75
116
 
76
117
  // Range Selection state
77
118
  isRangeSelecting = false;
78
- private rangeStartCell: { rowIndex: number, colId: string } | null = null;
119
+ private rangeStartCell: { rowIndex: number; colId: string } | null = null;
79
120
 
80
121
  // Side Bar state
81
122
  sideBarVisible = false;
@@ -85,7 +126,14 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
85
126
  activeContextMenu = false;
86
127
  contextMenuPosition = { x: 0, y: 0 };
87
128
  contextMenuItems: MenuItemDef[] = [];
88
- private contextMenuCell: { rowNode: IRowNode<TData>, column: Column } | null = null;
129
+ private contextMenuCell: { rowNode: IRowNode<TData>; column: Column } | null = null;
130
+
131
+ // Set Filter
132
+ activeSetFilter = false;
133
+ setFilterPosition = { x: 0, y: 0 };
134
+ setFilterValues: any[] = [];
135
+ setFilterValueFormatter?: (value: any) => string;
136
+ private activeSetFilterColumn: Column | null = null;
89
137
  private initialColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
90
138
 
91
139
  private gridApi!: GridApi<TData>;
@@ -95,7 +143,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
95
143
  private horizontalScrollListener?: (e: Event) => void;
96
144
  private resizeObserver?: ResizeObserver;
97
145
 
98
- constructor(@Inject(ChangeDetectorRef) private cdr: ChangeDetectorRef) {}
146
+ constructor(
147
+ @Inject(ChangeDetectorRef) private _cdr: ChangeDetectorRef,
148
+ private _elementRef: ElementRef<HTMLElement>
149
+ ) {}
99
150
 
100
151
  ngOnInit(): void {
101
152
  this.initialColumnDefs = this.columnDefs ? JSON.parse(JSON.stringify(this.columnDefs)) : null;
@@ -104,28 +155,60 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
104
155
 
105
156
  ngOnChanges(changes: SimpleChanges): void {
106
157
  // Handle rowData changes after initialization
107
- if (changes['rowData'] && !changes['rowData'].firstChange) {
108
- this.onRowDataChanged(changes['rowData'].currentValue);
158
+ if (changes.rowData && !changes.rowData.firstChange) {
159
+ this.onRowDataChanged(changes.rowData.currentValue);
109
160
  }
110
161
 
111
162
  // Handle columnDefs changes
112
- if (changes['columnDefs'] && !changes['columnDefs'].firstChange) {
113
- this.onColumnDefsChanged(changes['columnDefs'].currentValue);
163
+ if (changes.columnDefs && !changes.columnDefs.firstChange) {
164
+ this.onColumnDefsChanged(changes.columnDefs.currentValue);
114
165
  }
115
166
 
116
167
  // Handle gridOptions changes
117
- if (changes['gridOptions'] && !changes['gridOptions'].firstChange) {
118
- this.onGridOptionsChanged(changes['gridOptions'].currentValue);
168
+ if (changes.gridOptions && !changes.gridOptions.firstChange) {
169
+ this.onGridOptionsChanged(changes.gridOptions.currentValue);
170
+ }
171
+
172
+ // Handle rowSelection changes
173
+ if (changes.rowSelection && !changes.rowSelection.firstChange) {
174
+ if (this.gridApi) {
175
+ this.gridApi.setGridOption('rowSelection', changes.rowSelection.currentValue);
176
+ }
177
+ }
178
+
179
+ // Handle theme changes
180
+ if (changes.theme && !changes.theme.firstChange) {
181
+ // Apply theme CSS variables to the grid container
182
+ if (changes.theme.currentValue) {
183
+ applyThemeCSSVariables(changes.theme.currentValue, this._elementRef.nativeElement);
184
+ }
185
+
186
+ // Update canvas renderer theme if it's initialized
187
+ if (this.canvasRenderer) {
188
+ const convertedTheme = changes.theme.currentValue
189
+ ? convertThemeToGridTheme(changes.theme.currentValue)
190
+ : undefined;
191
+ this.canvasRenderer.setTheme(convertedTheme);
192
+ }
119
193
  }
120
194
  }
121
195
 
122
196
  ngAfterViewInit(): void {
123
197
  // Setup canvas renderer after view is initialized
124
198
  if (this.canvasRef && !this.canvasRenderer) {
199
+ // Convert theme from ThemeBuilder format to internal GridTheme format
200
+ const convertedTheme = this.theme ? convertThemeToGridTheme(this.theme) : undefined;
201
+
202
+ // Apply theme CSS variables to the grid container
203
+ if (this.theme) {
204
+ applyThemeCSSVariables(this.theme, this._elementRef.nativeElement);
205
+ }
206
+
125
207
  this.canvasRenderer = new CanvasRenderer(
126
208
  this.canvasRef.nativeElement,
127
209
  this.gridApi,
128
- this.rowHeight
210
+ this.rowHeight,
211
+ convertedTheme
129
212
  );
130
213
 
131
214
  // Wire up cell editing callback
@@ -141,28 +224,28 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
141
224
  // Range Selection Logic
142
225
  this.canvasRenderer.onMouseDown = (event, rowIndex, colId) => {
143
226
  if (event.button !== 0 || !colId || rowIndex === -1) return;
144
-
227
+
145
228
  const rangeSelectionEnabled = this.gridApi?.getGridOption('enableRangeSelection');
146
229
  if (!rangeSelectionEnabled) return;
147
230
 
148
231
  this.isRangeSelecting = true;
149
232
  this.rangeStartCell = { rowIndex, colId };
150
-
233
+
151
234
  // Clear previous selection if not holding Shift/Ctrl
152
235
  if (!event.shiftKey && !event.ctrlKey && !event.metaKey) {
153
236
  this.gridApi?.clearRangeSelection();
154
237
  }
155
238
  };
156
239
 
157
- this.canvasRenderer.onMouseMove = (event, rowIndex, colId) => {
240
+ this.canvasRenderer.onMouseMove = (_event, rowIndex, colId) => {
158
241
  if (!this.isRangeSelecting || !this.rangeStartCell || !colId || rowIndex === -1) return;
159
242
 
160
243
  const start = this.rangeStartCell;
161
244
  const end = { rowIndex, colId };
162
245
 
163
246
  const columns = this.canvasRenderer.getAllColumns();
164
- const startColIdx = columns.findIndex(c => c.colId === start.colId);
165
- const endColIdx = columns.findIndex(c => c.colId === end.colId);
247
+ const startColIdx = columns.findIndex((c) => c.colId === start.colId);
248
+ const endColIdx = columns.findIndex((c) => c.colId === end.colId);
166
249
 
167
250
  if (startColIdx === -1 || endColIdx === -1) return;
168
251
 
@@ -171,7 +254,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
171
254
  endRow: Math.max(start.rowIndex, end.rowIndex),
172
255
  startColumn: columns[Math.min(startColIdx, endColIdx)].colId,
173
256
  endColumn: columns[Math.max(startColIdx, endColIdx)].colId,
174
- columns: columns.slice(Math.min(startColIdx, endColIdx), Math.max(startColIdx, endColIdx) + 1)
257
+ columns: columns.slice(
258
+ Math.min(startColIdx, endColIdx),
259
+ Math.max(startColIdx, endColIdx) + 1
260
+ ),
175
261
  };
176
262
 
177
263
  this.gridApi?.addCellRange(range);
@@ -187,29 +273,32 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
187
273
  const rect = this.viewportRef.nativeElement.getBoundingClientRect();
188
274
  this.viewportHeight = rect.height || 500;
189
275
  this.canvasRenderer?.setViewportDimensions(rect.width, this.viewportHeight);
190
- this.canvasRenderer?.setTotalRowCount(this.rowData?.length || 0);
191
276
 
192
277
  // Synchronize horizontal scroll with DOM header
193
278
  this.horizontalScrollListener = () => {
194
279
  if (this.headerScrollableRef) {
195
- this.headerScrollableRef.nativeElement.scrollLeft = this.viewportRef.nativeElement.scrollLeft;
280
+ this.headerScrollableRef.nativeElement.scrollLeft =
281
+ this.viewportRef.nativeElement.scrollLeft;
196
282
  }
197
283
  if (this.headerScrollableFilterRef) {
198
- this.headerScrollableFilterRef.nativeElement.scrollLeft = this.viewportRef.nativeElement.scrollLeft;
284
+ this.headerScrollableFilterRef.nativeElement.scrollLeft =
285
+ this.viewportRef.nativeElement.scrollLeft;
199
286
  }
200
287
  };
201
-
202
- this.viewportRef.nativeElement.addEventListener('scroll', this.horizontalScrollListener, { passive: true });
288
+
289
+ this.viewportRef.nativeElement.addEventListener('scroll', this.horizontalScrollListener, {
290
+ passive: true,
291
+ });
203
292
 
204
293
  // Add ResizeObserver to handle sidebar toggling and other size changes
205
294
  if (typeof ResizeObserver !== 'undefined') {
206
- this.resizeObserver = new ResizeObserver(entries => {
295
+ this.resizeObserver = new ResizeObserver((entries) => {
207
296
  for (const entry of entries) {
208
297
  const { width, height } = entry.contentRect;
209
298
  this.viewportHeight = height;
210
299
  this.canvasRenderer?.setViewportDimensions(width, height);
211
300
  this.canvasRenderer?.render();
212
- this.cdr.detectChanges();
301
+ this._cdr.detectChanges();
213
302
  }
214
303
  });
215
304
  this.resizeObserver.observe(this.viewportRef.nativeElement);
@@ -235,24 +324,36 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
235
324
  }
236
325
 
237
326
  private initializeGrid(): void {
327
+ // Merge individual inputs into grid options if provided
328
+ const options = { ...this.gridOptions };
329
+ if (this.rowSelection) {
330
+ options.rowSelection = this.rowSelection;
331
+ }
332
+
238
333
  // Initialize grid API
239
- this.gridApi = this.gridService.createApi(this.columnDefs, this.rowData, this.gridOptions);
334
+ this.gridApi = this.gridService.createApi(this.columnDefs, this.rowData, options);
240
335
 
241
336
  // Listen for grid state changes from API (filters, sorts, options)
242
- this.gridService.gridStateChanged$
243
- .pipe(takeUntil(this.destroy$))
244
- .subscribe((event) => {
245
- if (event.type === 'optionChanged' && event.key === 'sideBar') {
246
- this.sideBarVisible = !!event.value;
337
+ this.gridService.gridStateChanged$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
338
+ if (event.type === 'optionChanged' && event.key === 'sideBar') {
339
+ this.sideBarVisible = !!event.value;
340
+ }
341
+ if (event.type === 'selectionChanged') {
342
+ this.updateSelectionState();
343
+
344
+ // Mark all rows as potentially dirty for selection change to ensure canvas redraws
345
+ // In a more optimized version, we'd only mark specific rows.
346
+ if (this.canvasRenderer) {
347
+ this.canvasRenderer.render(); // This calls markAllDirty and schedules render
247
348
  }
349
+ } else {
248
350
  this.canvasRenderer?.render();
249
- this.cdr.detectChanges();
250
- });
351
+ }
352
+ this._cdr.detectChanges();
353
+ });
251
354
 
252
- // Check if any column has checkbox selection
253
- this.showSelectionColumn = this.columnDefs?.some(col =>
254
- !('children' in col) && col.checkboxSelection
255
- ) || false;
355
+ // Selection column is now handled within the data columns
356
+ this.showSelectionColumn = false;
256
357
 
257
358
  // Canvas renderer will be initialized in ngAfterViewInit
258
359
 
@@ -277,7 +378,6 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
277
378
 
278
379
  if (this.gridApi) {
279
380
  this.gridApi.setRowData(newData || []);
280
- this.canvasRenderer?.setTotalRowCount(newData?.length || 0);
281
381
  this.canvasRenderer?.render();
282
382
  }
283
383
 
@@ -285,7 +385,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
285
385
  this.updateSelectionState();
286
386
 
287
387
  // Trigger change detection with OnPush
288
- this.cdr.detectChanges();
388
+ this._cdr.detectChanges();
289
389
  }
290
390
 
291
391
  private onColumnDefsChanged(newColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null): void {
@@ -295,22 +395,22 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
295
395
  this.gridApi.setColumnDefs(newColumnDefs);
296
396
  this.canvasRenderer?.render();
297
397
  }
298
-
299
- this.cdr.detectChanges();
398
+
399
+ this._cdr.detectChanges();
300
400
  }
301
401
 
302
402
  private onGridOptionsChanged(newOptions: GridOptions<TData> | null): void {
303
403
  this.gridOptions = newOptions;
304
404
  if (this.gridApi && newOptions) {
305
405
  // Update all options in the API
306
- Object.keys(newOptions).forEach(key => {
406
+ Object.keys(newOptions).forEach((key) => {
307
407
  this.gridApi.setGridOption(key as any, (newOptions as any)[key]);
308
408
  });
309
409
  this.canvasRenderer?.render();
310
410
  }
311
- this.cdr.detectChanges();
411
+ this._cdr.detectChanges();
312
412
  }
313
-
413
+
314
414
  getColumnWidth(col: Column | ColDef<TData> | ColGroupDef<TData>): number {
315
415
  if ('children' in col) {
316
416
  // Column group - sum children widths
@@ -321,26 +421,29 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
321
421
 
322
422
  getLeftPinnedColumns(): Column[] {
323
423
  if (!this.gridApi) return [];
324
- return this.gridApi.getAllColumns().filter(col => {
424
+ return this.gridApi.getAllColumns().filter((col) => {
325
425
  return col.visible && col.pinned === 'left';
326
426
  });
327
427
  }
328
428
 
329
429
  getRightPinnedColumns(): Column[] {
330
430
  if (!this.gridApi) return [];
331
- return this.gridApi.getAllColumns().filter(col => {
431
+ return this.gridApi.getAllColumns().filter((col) => {
332
432
  return col.visible && col.pinned === 'right';
333
433
  });
334
434
  }
335
435
 
336
436
  getNonPinnedColumns(): Column[] {
337
437
  if (!this.gridApi) return [];
338
- return this.gridApi.getAllColumns().filter(col => {
438
+ return this.gridApi.getAllColumns().filter((col) => {
339
439
  return col.visible && !col.pinned;
340
440
  });
341
441
  }
342
-
442
+
343
443
  isSortable(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
444
+ const colId = (col as any).colId || (col as any).field?.toString();
445
+ if (colId === 'ag-Grid-SelectionColumn') return false;
446
+
344
447
  // If it has children, it's a group and cannot be sorted directly
345
448
  if ('children' in col) return false;
346
449
 
@@ -351,34 +454,44 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
351
454
 
352
455
  // It's likely a Column object, look up its ColDef
353
456
  const colDef = this.getColumnDefForColumn(col as any);
354
- return colDef ? (colDef.sortable !== false) : true;
457
+ return colDef ? colDef.sortable !== false : true;
355
458
  }
356
-
459
+
357
460
  getHeaderName(col: Column | ColDef<TData> | ColGroupDef<TData>): string {
358
461
  if ('children' in col) {
359
462
  return col.headerName || '';
360
463
  }
464
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') {
465
+ return '';
466
+ }
361
467
  return col.headerName || (col as any).field?.toString() || '';
362
468
  }
363
-
469
+
364
470
  getSortIndicator(col: Column | ColDef<TData> | ColGroupDef<TData>): string {
365
471
  if ('children' in col || !col.sort) {
366
472
  return '';
367
473
  }
368
474
  return col.sort === 'asc' ? '▲' : '▼';
369
475
  }
370
-
476
+
371
477
  onHeaderClick(col: Column | ColDef<TData> | ColGroupDef<TData>): void {
478
+ if (this.isResizing) return;
479
+
480
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') {
481
+ // Selection is now handled by the checkbox directly to avoid resizing interference
482
+ return;
483
+ }
484
+
372
485
  if (!this.isSortable(col) || 'children' in col) {
373
486
  return;
374
487
  }
375
-
488
+
376
489
  // Toggle sort
377
490
  const currentSort = col.sort;
378
491
  const newSort = currentSort === 'asc' ? 'desc' : currentSort === 'desc' ? null : 'asc';
379
-
492
+
380
493
  const colId = (col as any).colId || (col as any).field?.toString() || '';
381
-
494
+
382
495
  // Update the column directly if it's a Column object
383
496
  if ('colId' in col && !(col as any).children) {
384
497
  (col as any).sort = newSort;
@@ -390,7 +503,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
390
503
 
391
504
  // --- Header Menu Logic ---
392
505
 
506
+ headerMenuItems: MenuItemDef[] = [];
507
+
393
508
  hasHeaderMenu(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
509
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') return false;
394
510
  if ('children' in col) return false;
395
511
  const colDef = this.getColumnDefForColumn(col as any);
396
512
  return colDef ? colDef.suppressHeaderMenuButton !== true : true;
@@ -398,36 +514,130 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
398
514
 
399
515
  onHeaderMenuClick(event: MouseEvent, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
400
516
  event.stopPropagation();
401
-
517
+
402
518
  if (this.activeHeaderMenu === col) {
403
519
  this.closeHeaderMenu();
404
520
  return;
405
521
  }
406
522
 
407
523
  this.activeHeaderMenu = col;
408
-
524
+ this.headerMenuItems = this.getHeaderMenuItems(col as Column);
525
+
409
526
  // Position menu below the icon using fixed (viewport) coordinates
410
527
  const target = event.target as HTMLElement;
411
528
  const rect = target.getBoundingClientRect();
412
-
529
+
413
530
  let x = rect.right - 200; // Align right, assuming menu width ~200px
414
531
  let y = rect.bottom + 4;
415
532
 
416
533
  // Prevent menu from going off-screen
417
534
  if (x < 0) x = 0;
418
535
  if (x + 200 > window.innerWidth) x = window.innerWidth - 200;
419
- if (y + 250 > window.innerHeight) { // Assuming max menu height ~250px
420
- y = rect.top - 250; // Show above if overflows bottom
536
+
537
+ // Check if menu would overflow bottom
538
+ const estimatedHeight = this.headerMenuItems.length * 30 + 20;
539
+ if (y + estimatedHeight > window.innerHeight) {
540
+ y = Math.max(0, rect.top - estimatedHeight);
421
541
  }
422
-
542
+
423
543
  this.headerMenuPosition = { x, y };
424
-
425
- this.cdr.detectChanges();
544
+
545
+ this._cdr.detectChanges();
546
+ }
547
+
548
+ private getHeaderMenuItems(col: Column): MenuItemDef[] {
549
+ const items: MenuItemDef[] = [];
550
+
551
+ // 1. Sort items
552
+ items.push({
553
+ name: 'Sort Ascending',
554
+ icon: '↑',
555
+ action: () => this.sortColumnMenu('asc'),
556
+ });
557
+ items.push({
558
+ name: 'Sort Descending',
559
+ icon: '↓',
560
+ action: () => this.sortColumnMenu('desc'),
561
+ });
562
+ items.push({
563
+ name: 'Clear Sort',
564
+ icon: '✕',
565
+ action: () => this.sortColumnMenu(null),
566
+ });
567
+
568
+ items.push({ name: '', action: () => {}, separator: true });
569
+
570
+ // 2. Filter items
571
+ const colDef = this.getColumnDefForColumn(col);
572
+ if (colDef && colDef.filter !== false) {
573
+ const filterType = colDef.filter || 'text';
574
+
575
+ if (filterType === 'set') {
576
+ items.push({
577
+ name: 'Filter...',
578
+ icon: 'Y',
579
+ action: () => {
580
+ this.openSetFilter(null, col, { ...this.headerMenuPosition });
581
+ this.closeHeaderMenu();
582
+ },
583
+ });
584
+ } else {
585
+ items.push({
586
+ name: 'Filter...',
587
+ icon: 'Y',
588
+ action: () => {
589
+ this.openFilterPopup(null, col, { ...this.headerMenuPosition });
590
+ this.closeHeaderMenu();
591
+ },
592
+ });
593
+ }
594
+ }
595
+
596
+ items.push({ name: '', action: () => {}, separator: true });
597
+
598
+ // 3. Pinning items
599
+ items.push({
600
+ name: 'Pin Left',
601
+ icon: '«',
602
+ action: () => this.pinColumnMenu('left'),
603
+ });
604
+ items.push({
605
+ name: 'Pin Right',
606
+ icon: '»',
607
+ action: () => this.pinColumnMenu('right'),
608
+ });
609
+ items.push({
610
+ name: 'Unpin',
611
+ icon: '↺',
612
+ action: () => this.pinColumnMenu(null),
613
+ });
614
+
615
+ items.push({ name: '', action: () => {}, separator: true });
616
+
617
+ // 4. Hide item
618
+ items.push({
619
+ name: 'Hide Column',
620
+ icon: 'ø',
621
+ action: () => this.hideColumnMenu(),
622
+ });
623
+
624
+ return items;
625
+ }
626
+
627
+ public clearColumnFilter(col: Column): void {
628
+ const field = col.field;
629
+ if (!field || !this.gridApi) return;
630
+
631
+ const currentModel = this.gridApi.getFilterModel();
632
+ delete (currentModel as any)[field];
633
+ this.gridApi.setFilterModel(currentModel);
634
+ this.closeHeaderMenu();
635
+ this.closeFilterPopup();
426
636
  }
427
637
 
428
638
  closeHeaderMenu(): void {
429
639
  this.activeHeaderMenu = null;
430
- this.cdr.detectChanges();
640
+ this._cdr.detectChanges();
431
641
  }
432
642
 
433
643
  onContainerClick(event: MouseEvent): void {
@@ -448,19 +658,19 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
448
658
 
449
659
  onCanvasContextMenu(event: MouseEvent): void {
450
660
  event.preventDefault();
451
-
661
+
452
662
  // Get hit test from canvas renderer to know which cell was clicked
453
663
  const hitTest = this.canvasRenderer.getHitTestResult(event);
454
664
  if (!hitTest || hitTest.rowIndex === -1) return;
455
-
665
+
456
666
  const rowNode = this.gridApi.getDisplayedRowAtIndex(hitTest.rowIndex);
457
- const columns = this.gridApi.getAllColumns().filter(col => col.visible);
667
+ const columns = this.gridApi.getAllColumns().filter((col) => col.visible);
458
668
  const column = columns[hitTest.columnIndex];
459
-
669
+
460
670
  if (!rowNode || !column) return;
461
-
671
+
462
672
  this.contextMenuCell = { rowNode, column };
463
-
673
+
464
674
  // Resolve menu items via API if provided
465
675
  const getContextMenuItems = this.gridApi.getGridOption('getContextMenuItems');
466
676
  if (getContextMenuItems) {
@@ -469,20 +679,25 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
469
679
  column: column,
470
680
  api: this.gridApi,
471
681
  type: 'cell',
472
- event: event
682
+ event: event,
473
683
  };
474
684
  this.contextMenuItems = this.resolveContextMenuItems(getContextMenuItems(params));
475
685
  } else {
476
686
  // Fallback to defaults if no callback provided
477
687
  this.contextMenuItems = this.resolveContextMenuItems([
478
- 'copy', 'copyWithHeaders', 'separator', 'export', 'separator', 'resetColumns'
688
+ 'copy',
689
+ 'copyWithHeaders',
690
+ 'separator',
691
+ 'export',
692
+ 'separator',
693
+ 'resetColumns',
479
694
  ]);
480
695
  }
481
696
 
482
697
  if (this.contextMenuItems.length === 0) return;
483
698
 
484
699
  this.activeContextMenu = true;
485
-
700
+
486
701
  // Position menu at mouse coordinates (fixed/viewport)
487
702
  let x = event.clientX;
488
703
  let y = event.clientY;
@@ -492,21 +707,21 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
492
707
  if (y + 200 > window.innerHeight) y = window.innerHeight - 200;
493
708
 
494
709
  this.contextMenuPosition = { x, y };
495
-
710
+
496
711
  // Select the row
497
712
  this.gridApi.deselectAll();
498
713
  rowNode.selected = true;
499
714
  this.updateSelectionState();
500
715
  this.canvasRenderer?.render();
501
716
  this.selectionChanged.emit(this.gridApi.getSelectedRows());
502
-
503
- this.cdr.detectChanges();
717
+
718
+ this._cdr.detectChanges();
504
719
  }
505
720
 
506
721
  private resolveContextMenuItems(items: (DefaultMenuItem | MenuItemDef)[]): MenuItemDef[] {
507
722
  const resolved: MenuItemDef[] = [];
508
-
509
- items.forEach(item => {
723
+
724
+ items.forEach((item) => {
510
725
  if (typeof item === 'string') {
511
726
  const defaultItem = this.getDefaultMenuItem(item);
512
727
  if (defaultItem) resolved.push(defaultItem);
@@ -514,7 +729,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
514
729
  resolved.push(item);
515
730
  }
516
731
  });
517
-
732
+
518
733
  return resolved;
519
734
  }
520
735
 
@@ -523,17 +738,18 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
523
738
  case 'copy':
524
739
  return { name: 'Copy Cell', action: () => this.copyContextMenuCell(), icon: '📋' };
525
740
  case 'copyWithHeaders':
526
- return this.hasRangeSelection() ?
527
- { name: 'Copy with Headers', action: () => this.copyRangeWithHeaders(), icon: '📋' } : null;
741
+ return this.hasRangeSelection()
742
+ ? { name: 'Copy with Headers', action: () => this.copyRangeWithHeaders(), icon: '📋' }
743
+ : null;
528
744
  case 'export':
529
- return {
530
- name: 'Export',
531
- action: () => {},
745
+ return {
746
+ name: 'Export',
747
+ action: () => {},
532
748
  icon: '⤓',
533
749
  subMenu: [
534
750
  { name: 'Export to CSV', action: () => this.exportCSV() },
535
- { name: 'Export to Excel (.xlsx)', action: () => this.exportExcel() }
536
- ]
751
+ { name: 'Export to Excel (.xlsx)', action: () => this.exportExcel() },
752
+ ],
537
753
  };
538
754
  case 'resetColumns':
539
755
  return { name: 'Reset Columns', action: () => this.resetColumns(), icon: '⟲' };
@@ -547,7 +763,224 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
547
763
  closeContextMenu(): void {
548
764
  this.activeContextMenu = false;
549
765
  this.contextMenuCell = null;
550
- this.cdr.detectChanges();
766
+ this._cdr.detectChanges();
767
+ }
768
+
769
+ // Set Filter Methods
770
+ isSetFilter(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
771
+ if ('children' in col) return false;
772
+ const colDef = col as ColDef<TData>;
773
+ return colDef.filter === 'set';
774
+ }
775
+
776
+ openSetFilter(
777
+ event: MouseEvent | null,
778
+ col: Column | ColDef<TData>,
779
+ position?: { x: number; y: number }
780
+ ): void {
781
+ if (event) {
782
+ event.stopPropagation();
783
+ event.preventDefault();
784
+ }
785
+
786
+ this.activeSetFilterColumn = col as Column;
787
+
788
+ const field = col.field;
789
+ if (!field || !this.gridApi) return;
790
+
791
+ this.setFilterValues = this.gridService.getUniqueValues(field as string);
792
+ const colDef = 'field' in col ? (col as ColDef<TData>) : null;
793
+ this.setFilterValueFormatter = colDef?.valueFormatter
794
+ ? (colDef.valueFormatter as any)
795
+ : undefined;
796
+
797
+ if (position) {
798
+ this.setFilterPosition = position;
799
+ } else if (event) {
800
+ const rect = (event.target as HTMLElement).getBoundingClientRect();
801
+ this.setFilterPosition = {
802
+ x: rect.left,
803
+ y: rect.bottom + 5,
804
+ };
805
+ }
806
+
807
+ setTimeout(() => {
808
+ this.activeSetFilter = true;
809
+ this._cdr.detectChanges();
810
+ });
811
+ }
812
+
813
+ closeSetFilter(): void {
814
+ this.activeSetFilter = false;
815
+ this.activeSetFilterColumn = null;
816
+ this._cdr.detectChanges();
817
+ }
818
+
819
+ // Filter Popup state
820
+ activeFilterPopup = false;
821
+ activeFilterPopupColumn: Column | null = null;
822
+ activeFilterPopupType: 'text' | 'number' | 'date' | 'boolean' | 'set' | 'multiFilter' = 'text';
823
+ activeFilterOperator: string = 'contains';
824
+ filterPopupPosition = { x: 0, y: 0 };
825
+ filterValue1: string = '';
826
+ filterValue2: string = '';
827
+
828
+ readonly textFilterOperators = [
829
+ { value: 'contains', label: 'Contains' },
830
+ { value: 'notContains', label: 'Not contains' },
831
+ { value: 'equals', label: 'Equals' },
832
+ { value: 'notEquals', label: 'Not equals' },
833
+ { value: 'startsWith', label: 'Starts with' },
834
+ { value: 'endsWith', label: 'Ends with' },
835
+ { value: 'blank', label: 'Blank' },
836
+ { value: 'notBlank', label: 'Not blank' },
837
+ ];
838
+
839
+ readonly numberFilterOperators = [
840
+ { value: 'equals', label: 'Equals' },
841
+ { value: 'notEquals', label: 'Not equals' },
842
+ { value: 'greaterThan', label: 'Greater than' },
843
+ { value: 'greaterThanOrEqual', label: 'Greater than or equals' },
844
+ { value: 'lessThan', label: 'Less than' },
845
+ { value: 'lessThanOrEqual', label: 'Less than or equals' },
846
+ { value: 'inRange', label: 'In range' },
847
+ { value: 'blank', label: 'Blank' },
848
+ { value: 'notBlank', label: 'Not blank' },
849
+ ];
850
+
851
+ onFilterPopupOperatorChange(operator: string): void {
852
+ this.activeFilterOperator = operator;
853
+ this.applyPopupFilter();
854
+ }
855
+
856
+ onFilterPopupInput(event: Event, isSecondValue: boolean = false): void {
857
+ const value = (event.target as HTMLInputElement).value;
858
+ if (isSecondValue) {
859
+ this.filterValue2 = value;
860
+ } else {
861
+ this.filterValue1 = value;
862
+ }
863
+ this.applyPopupFilter();
864
+ }
865
+
866
+ private applyPopupFilter(): void {
867
+ if (!this.activeFilterPopupColumn || !this.gridApi) return;
868
+
869
+ const col = this.activeFilterPopupColumn;
870
+ const field = col.field;
871
+ if (!field) return;
872
+
873
+ const currentModel = this.gridApi.getFilterModel();
874
+
875
+ if (this.activeFilterOperator === 'blank' || this.activeFilterOperator === 'notBlank') {
876
+ currentModel[col.colId] = {
877
+ filterType: this.activeFilterPopupType,
878
+ type: this.activeFilterOperator,
879
+ };
880
+ } else {
881
+ const value = this.filterValue1;
882
+ if (!value && this.activeFilterOperator !== 'inRange') {
883
+ delete currentModel[col.colId];
884
+ } else {
885
+ const filterModel: any = {
886
+ filterType: this.activeFilterPopupType,
887
+ type: this.activeFilterOperator,
888
+ filter: value,
889
+ };
890
+
891
+ if (this.activeFilterOperator === 'inRange') {
892
+ filterModel.filterTo = this.filterValue2;
893
+ }
894
+
895
+ currentModel[col.colId] = filterModel;
896
+ }
897
+ }
898
+
899
+ this.gridApi.setFilterModel(currentModel);
900
+ this.canvasRenderer?.render();
901
+ this._cdr.detectChanges();
902
+ }
903
+
904
+ openFilterPopup(
905
+ event: MouseEvent | null,
906
+ col: Column,
907
+ position?: { x: number; y: number }
908
+ ): void {
909
+ this.activeFilterPopupColumn = col;
910
+ const colDef = this.getColumnDefForColumn(col);
911
+ this.activeFilterPopupType = colDef?.filter === 'number' ? 'number' : 'text';
912
+
913
+ // Initialize operator and values from current model or default
914
+ const model = this.gridApi?.getFilterModel()[col.colId] as any;
915
+ this.activeFilterOperator =
916
+ model?.type || (this.activeFilterPopupType === 'number' ? 'equals' : 'contains');
917
+ this.filterValue1 = model?.filter || '';
918
+ this.filterValue2 = model?.filterTo || '';
919
+
920
+ if (position) {
921
+ this.filterPopupPosition = position;
922
+ } else if (event) {
923
+ const rect = (event.target as HTMLElement).getBoundingClientRect();
924
+ this.filterPopupPosition = { x: rect.left, y: rect.bottom + 5 };
925
+ }
926
+
927
+ setTimeout(() => {
928
+ this.activeFilterPopup = true;
929
+ this._cdr.detectChanges();
930
+ });
931
+ }
932
+
933
+ closeFilterPopup(): void {
934
+ this.activeFilterPopup = false;
935
+ this.activeFilterPopupColumn = null;
936
+ this._cdr.detectChanges();
937
+ }
938
+
939
+ onSetFilterChanged(values: any[]): void {
940
+ if (!this.activeSetFilterColumn || !this.gridApi) return;
941
+
942
+ const field = this.activeSetFilterColumn.field;
943
+ if (!field) return;
944
+
945
+ if (values.length === 0) {
946
+ const currentModel = this.gridApi.getFilterModel();
947
+ delete (currentModel as any)[field];
948
+ this.gridApi.setFilterModel(currentModel);
949
+ } else {
950
+ this.gridApi.setFilterModel({
951
+ ...this.gridApi.getFilterModel(),
952
+ [field]: {
953
+ filterType: 'set',
954
+ values: values,
955
+ },
956
+ });
957
+ }
958
+
959
+ this.closeSetFilter();
960
+ this.canvasRenderer?.render();
961
+ }
962
+
963
+ hasSetFilterValue(col: Column | ColDef<TData>): boolean {
964
+ if (!this.gridApi) return false;
965
+ const field = 'field' in col ? col.field : null;
966
+ if (!field) return false;
967
+
968
+ const model = this.gridApi.getFilterModel();
969
+ const filter = (model as any)[field];
970
+ return filter && filter.filterType === 'set' && filter.values && filter.values.length > 0;
971
+ }
972
+
973
+ getSetFilterCount(col: Column | ColDef<TData>): number {
974
+ if (!this.gridApi) return 0;
975
+ const field = 'field' in col ? col.field : null;
976
+ if (!field) return 0;
977
+
978
+ const model = this.gridApi.getFilterModel();
979
+ const filter = (model as any)[field];
980
+ if (filter && filter.filterType === 'set' && Array.isArray(filter.values)) {
981
+ return filter.values.length;
982
+ }
983
+ return 0;
551
984
  }
552
985
 
553
986
  // Side Bar Methods
@@ -557,7 +990,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
557
990
  } else {
558
991
  this.activeToolPanel = panel;
559
992
  }
560
- this.cdr.detectChanges();
993
+ this._cdr.detectChanges();
561
994
  }
562
995
 
563
996
  toggleColumnVisibility(col: Column): void {
@@ -566,7 +999,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
566
999
  colDef.hide = col.visible; // Toggle
567
1000
  this.initializeGrid(); // Re-initialize to handle visibility changes correctly
568
1001
  this.canvasRenderer?.render();
569
- this.cdr.detectChanges();
1002
+ this._cdr.detectChanges();
570
1003
  }
571
1004
  }
572
1005
 
@@ -582,7 +1015,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
582
1015
 
583
1016
  // Map back to ColDefs
584
1017
  const newDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
585
- columns.forEach(col => {
1018
+ columns.forEach((col) => {
586
1019
  const def = this.getColumnDefForColumn(col);
587
1020
  if (def) newDefs.push(def);
588
1021
  });
@@ -592,10 +1025,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
592
1025
 
593
1026
  copyContextMenuCell(): void {
594
1027
  if (!this.contextMenuCell || !this.contextMenuCell.column.field) return;
595
-
1028
+
596
1029
  const val = (this.contextMenuCell.rowNode.data as any)[this.contextMenuCell.column.field];
597
1030
  if (val !== undefined && val !== null) {
598
- navigator.clipboard.writeText(String(val)).catch(err => {
1031
+ navigator.clipboard.writeText(String(val)).catch((err) => {
599
1032
  console.error('Failed to copy text: ', err);
600
1033
  });
601
1034
  }
@@ -612,20 +1045,22 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
612
1045
 
613
1046
  const range = ranges[0];
614
1047
  const columns = range.columns;
615
-
616
- let text = columns.map(c => this.getHeaderName(c)).join('\t') + '\n';
1048
+
1049
+ let text = `${columns.map((c) => this.getHeaderName(c)).join('\t')}\n`;
617
1050
 
618
1051
  for (let i = range.startRow; i <= range.endRow; i++) {
619
1052
  const node = this.gridApi.getDisplayedRowAtIndex(i);
620
1053
  if (node) {
621
- text += columns.map(c => {
622
- const val = (node.data as any)[c.field || ''];
623
- return val !== null && val !== undefined ? String(val) : '';
624
- }).join('\t') + '\n';
1054
+ text += `${columns
1055
+ .map((c) => {
1056
+ const val = (node.data as any)[c.field || ''];
1057
+ return val !== null && val !== undefined ? String(val) : '';
1058
+ })
1059
+ .join('\t')}\n`;
625
1060
  }
626
1061
  }
627
1062
 
628
- navigator.clipboard.writeText(text).catch(err => {
1063
+ navigator.clipboard.writeText(text).catch((err) => {
629
1064
  console.error('Failed to copy range: ', err);
630
1065
  });
631
1066
  this.closeContextMenu();
@@ -646,7 +1081,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
646
1081
  // Deep copy back the original defs
647
1082
  const restored = JSON.parse(JSON.stringify(this.initialColumnDefs));
648
1083
  this.onColumnDefsChanged(restored);
649
-
1084
+
650
1085
  // Also clear sort model
651
1086
  this.gridApi.setSortModel([]);
652
1087
  }
@@ -655,10 +1090,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
655
1090
 
656
1091
  sortColumnMenu(direction: 'asc' | 'desc' | null): void {
657
1092
  if (!this.activeHeaderMenu) return;
658
-
1093
+
659
1094
  const col = this.activeHeaderMenu as any;
660
1095
  const colId = col.colId || col.field?.toString() || '';
661
-
1096
+
662
1097
  // Update original ColDef to ensure persistence
663
1098
  const colDef = this.getColumnDefForColumn(col);
664
1099
  if (colDef) {
@@ -667,44 +1102,44 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
667
1102
 
668
1103
  this.gridApi.setSortModel(direction ? [{ colId, sort: direction }] : []);
669
1104
  this.canvasRenderer?.render();
670
-
1105
+
671
1106
  this.closeHeaderMenu();
672
1107
  }
673
1108
 
674
1109
  hideColumnMenu(): void {
675
1110
  if (!this.activeHeaderMenu) return;
676
-
1111
+
677
1112
  const col = this.activeHeaderMenu as any;
678
-
1113
+
679
1114
  // Update the original column definition
680
1115
  const colDef = this.getColumnDefForColumn(col);
681
1116
  if (colDef) {
682
1117
  colDef.hide = true;
683
1118
  }
684
-
1119
+
685
1120
  // Create new array to trigger change detection and API update
686
1121
  if (this.columnDefs) {
687
1122
  this.onColumnDefsChanged([...this.columnDefs]);
688
1123
  }
689
-
1124
+
690
1125
  this.closeHeaderMenu();
691
1126
  }
692
1127
 
693
1128
  pinColumnMenu(pin: 'left' | 'right' | null): void {
694
1129
  if (!this.activeHeaderMenu) return;
695
-
1130
+
696
1131
  const col = this.activeHeaderMenu as any;
697
-
1132
+
698
1133
  // Update the original column definition
699
1134
  const colDef = this.getColumnDefForColumn(col);
700
1135
  if (colDef) {
701
1136
  colDef.pinned = pin as any;
702
1137
  }
703
-
1138
+
704
1139
  if (this.columnDefs) {
705
1140
  this.onColumnDefsChanged([...this.columnDefs]);
706
1141
  }
707
-
1142
+
708
1143
  this.closeHeaderMenu();
709
1144
  }
710
1145
 
@@ -718,8 +1153,8 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
718
1153
 
719
1154
  const containerMap: { [key: string]: any[] } = {
720
1155
  'left-pinned': left,
721
- 'scrollable': center,
722
- 'right-pinned': right
1156
+ scrollable: center,
1157
+ 'right-pinned': right,
723
1158
  };
724
1159
 
725
1160
  const previousContainerData = containerMap[event.previousContainer.id];
@@ -739,31 +1174,32 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
739
1174
  const movedCol = currentContainerData[event.currentIndex] as Column;
740
1175
  const colDef = this.getColumnDefForColumn(movedCol);
741
1176
  if (colDef) {
742
- colDef.pinned = pinType === 'none' ? null : pinType as any;
1177
+ colDef.pinned = pinType === 'none' ? null : (pinType as any);
743
1178
  }
744
1179
  }
745
1180
 
746
1181
  // Map internal Columns back to their original ColDefs in the new order
747
1182
  const orderedVisibleColDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
748
- [...left, ...center, ...right].forEach(col => {
1183
+ [...left, ...center, ...right].forEach((col) => {
749
1184
  const def = this.getColumnDefForColumn(col);
750
1185
  if (def) orderedVisibleColDefs.push(def);
751
1186
  });
752
1187
 
753
1188
  // Reconstruct full columnDefs array, maintaining hidden columns
754
- const hidden = this.columnDefs.filter(c => {
1189
+ const hidden = this.columnDefs.filter((c) => {
755
1190
  if ('children' in c) return false;
756
1191
  return (c as ColDef).hide;
757
1192
  });
758
-
1193
+
759
1194
  const newDefs = [...orderedVisibleColDefs, ...hidden];
760
-
1195
+
761
1196
  this.onColumnDefsChanged(newDefs);
762
1197
  }
763
1198
 
764
1199
  // --- Column Resizing Logic ---
765
1200
 
766
1201
  isResizable(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
1202
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') return true;
767
1203
  if ('children' in col) return false;
768
1204
  const colDef = this.getColumnDefForColumn(col as any);
769
1205
  return colDef ? colDef.resizable !== false : true;
@@ -794,10 +1230,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
794
1230
 
795
1231
  const deltaX = event.clientX - this.resizeStartX;
796
1232
  const newWidth = Math.max(20, this.resizeStartWidth + deltaX);
797
-
1233
+
798
1234
  // Update internal column width
799
1235
  this.resizeColumn.width = newWidth;
800
-
1236
+
801
1237
  // Update original ColDef
802
1238
  const colDef = this.getColumnDefForColumn(this.resizeColumn);
803
1239
  if (colDef) {
@@ -806,7 +1242,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
806
1242
 
807
1243
  // Force re-render
808
1244
  this.canvasRenderer?.render();
809
- this.cdr.detectChanges();
1245
+ this._cdr.detectChanges();
810
1246
  }
811
1247
 
812
1248
  private onResizeMouseUp(): void {
@@ -818,24 +1254,28 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
818
1254
 
819
1255
  hasFloatingFilters(): boolean {
820
1256
  if (this.gridApi?.getGridOption('floatingFilter')) return true;
821
-
1257
+ if (this.gridOptions?.defaultColDef?.floatingFilter) return true;
1258
+
822
1259
  if (!this.columnDefs) return false;
823
- return this.columnDefs.some(col => {
1260
+ const hasAny = this.columnDefs.some((col) => {
824
1261
  if ('children' in col) {
825
- return col.children.some(child => 'floatingFilter' in child && child.floatingFilter);
1262
+ return col.children.some((child) => (child as any).floatingFilter);
826
1263
  }
827
- return col.floatingFilter;
1264
+ return (col as any).floatingFilter;
828
1265
  });
1266
+ return hasAny;
829
1267
  }
830
1268
 
831
1269
  isFloatingFilterEnabled(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
832
1270
  const colDef = this.getColumnDefForColumn(col as any);
833
1271
  if (!colDef || 'children' in colDef) return false;
834
- if (!colDef.filter) return false;
835
-
1272
+
1273
+ const filter = colDef.filter;
1274
+ if (!filter) return false;
1275
+
836
1276
  if (colDef.floatingFilter === true) return true;
837
1277
  if (colDef.floatingFilter === false) return false;
838
-
1278
+
839
1279
  return !!this.gridApi?.getGridOption('floatingFilter');
840
1280
  }
841
1281
 
@@ -858,25 +1298,27 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
858
1298
  onFloatingFilterInput(event: Event, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
859
1299
  const colDef = this.getColumnDefForColumn(col as any);
860
1300
  if (!colDef || 'children' in colDef) return;
861
-
1301
+
862
1302
  const input = event.target as HTMLInputElement;
863
1303
  const value = input.value;
864
1304
  const colId = (col as any).colId || (col as any).field?.toString() || '';
865
1305
 
866
- this.cdr.detectChanges(); // Update clear button visibility immediately
1306
+ this._cdr.detectChanges(); // Update clear button visibility immediately
867
1307
 
868
1308
  clearTimeout(this.filterTimeout);
869
1309
  this.filterTimeout = setTimeout(() => {
870
1310
  const currentModel = this.gridApi.getFilterModel();
871
-
872
- if (!value) {
1311
+ const existingFilter = (currentModel[colId] || {}) as any;
1312
+
1313
+ if (!value && existingFilter.type !== 'blank' && existingFilter.type !== 'notBlank') {
873
1314
  delete currentModel[colId];
874
1315
  } else {
875
1316
  const filterType = this.getFilterTypeFromCol(colDef);
876
1317
  currentModel[colId] = {
1318
+ ...existingFilter,
877
1319
  filterType: filterType as any,
878
- type: filterType === 'text' ? 'contains' : 'equals',
879
- filter: value
1320
+ type: existingFilter.type || (filterType === 'text' ? 'contains' : 'equals'),
1321
+ filter: value,
880
1322
  };
881
1323
  }
882
1324
 
@@ -900,25 +1342,31 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
900
1342
  return model[colId]?.filter || '';
901
1343
  }
902
1344
 
903
- hasFilterValue(col: Column | ColDef<TData> | ColGroupDef<TData>, input: HTMLInputElement): boolean {
1345
+ hasFilterValue(
1346
+ col: Column | ColDef<TData> | ColGroupDef<TData>,
1347
+ _input: HTMLInputElement
1348
+ ): boolean {
904
1349
  return !!this.getFloatingFilterValue(col);
905
1350
  }
906
1351
 
907
- clearFloatingFilter(col: Column | ColDef<TData> | ColGroupDef<TData>, input: HTMLInputElement): void {
1352
+ clearFloatingFilter(
1353
+ col: Column | ColDef<TData> | ColGroupDef<TData>,
1354
+ input: HTMLInputElement
1355
+ ): void {
908
1356
  const colDef = this.getColumnDefForColumn(col as any);
909
1357
  if (!colDef || 'children' in colDef) return;
910
-
1358
+
911
1359
  input.value = '';
912
1360
  const colId = (col as any).colId || (col as any).field?.toString() || '';
913
-
1361
+
914
1362
  const currentModel = this.gridApi.getFilterModel();
915
1363
  delete currentModel[colId];
916
-
1364
+
917
1365
  this.gridApi.setFilterModel(currentModel);
918
1366
  this.canvasRenderer?.render();
919
- this.cdr.detectChanges();
1367
+ this._cdr.detectChanges();
920
1368
  }
921
-
1369
+
922
1370
  // Public API methods
923
1371
  getApi(): GridApi<TData> {
924
1372
  return this.gridApi;
@@ -936,42 +1384,42 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
936
1384
  startEditing(rowIndex: number, colId: string): void {
937
1385
  const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
938
1386
  const column = this.gridApi.getColumn(colId);
939
-
1387
+
940
1388
  // Prevent editing on group rows or missing data/column
941
1389
  if (!rowNode || rowNode.group || !column || !column.field) return;
942
-
1390
+
943
1391
  // Check if cell is editable
944
1392
  const colDef = this.getColumnDefForColumn(column);
945
1393
  if (colDef && colDef.editable === false) return;
946
-
1394
+
947
1395
  // If already editing another cell, stop it first
948
1396
  if (this.isEditing) {
949
1397
  this.stopEditing(true);
950
1398
  }
951
1399
 
952
1400
  const value = (rowNode.data as any)[column.field];
953
-
1401
+
954
1402
  this.editingRowNode = rowNode;
955
1403
  this.editingColDef = colDef;
956
1404
  this.editingValue = value !== null && value !== undefined ? String(value) : '';
957
-
1405
+
958
1406
  // Calculate editor position based on row and column
959
- const columns = this.gridApi.getAllColumns().filter(c => c.visible);
1407
+ const columns = this.gridApi.getAllColumns().filter((c) => c.visible);
960
1408
  let x = 0;
961
1409
  for (const col of columns) {
962
1410
  if (col.colId === colId) break;
963
1411
  x += col.width;
964
1412
  }
965
-
1413
+
966
1414
  this.editorPosition = {
967
1415
  x: x - this.canvasRenderer.currentScrollLeft,
968
- y: (rowIndex * this.rowHeight) - this.canvasRenderer.currentScrollTop,
1416
+ y: rowIndex * this.rowHeight - this.canvasRenderer.currentScrollTop,
969
1417
  width: column.width,
970
- height: this.rowHeight
1418
+ height: this.rowHeight,
971
1419
  };
972
-
1420
+
973
1421
  this.isEditing = true;
974
-
1422
+
975
1423
  // Focus input after view update
976
1424
  setTimeout(() => {
977
1425
  if (this.editorInputRef) {
@@ -984,7 +1432,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
984
1432
 
985
1433
  stopEditing(save: boolean = true): void {
986
1434
  if (!this.isEditing) return;
987
-
1435
+
988
1436
  const rowNode = this.editingRowNode;
989
1437
  const colDef = this.editingColDef;
990
1438
 
@@ -1007,7 +1455,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1007
1455
  data: rowNode.data,
1008
1456
  node: rowNode,
1009
1457
  colDef,
1010
- api: this.gridApi
1458
+ api: this.gridApi,
1011
1459
  });
1012
1460
  }
1013
1461
 
@@ -1019,7 +1467,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1019
1467
  data: rowNode.data,
1020
1468
  node: rowNode,
1021
1469
  colDef,
1022
- api: this.gridApi
1470
+ api: this.gridApi,
1023
1471
  });
1024
1472
  } else if (field) {
1025
1473
  // Default: update data directly
@@ -1028,9 +1476,9 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1028
1476
 
1029
1477
  // Update via transaction
1030
1478
  this.gridApi.applyTransaction({
1031
- update: [rowNode.data]
1479
+ update: [rowNode.data],
1032
1480
  });
1033
-
1481
+
1034
1482
  // Trigger callback
1035
1483
  if (colDef.onCellValueChanged) {
1036
1484
  const column = this.gridApi.getColumn(colDef.colId || field || '');
@@ -1040,18 +1488,18 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1040
1488
  oldValue,
1041
1489
  data: rowNode.data,
1042
1490
  node: rowNode,
1043
- column
1491
+ column,
1044
1492
  });
1045
1493
  }
1046
1494
  }
1047
-
1495
+
1048
1496
  this.canvasRenderer?.render();
1049
1497
  }
1050
-
1498
+
1051
1499
  this.isEditing = false;
1052
1500
  this.editingRowNode = null;
1053
1501
  this.editingColDef = null;
1054
- this.cdr.detectChanges();
1502
+ this._cdr.detectChanges();
1055
1503
  }
1056
1504
 
1057
1505
  onEditorInput(event: Event): void {
@@ -1070,7 +1518,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1070
1518
  event.preventDefault();
1071
1519
  const currentRowIndex = this.editingRowNode?.displayedRowIndex ?? -1;
1072
1520
  const currentColId = this.editingColDef?.colId || this.editingColDef?.field?.toString() || '';
1073
-
1521
+
1074
1522
  this.stopEditing(true);
1075
1523
 
1076
1524
  // Standard AG Grid Tab behavior: move to next cell
@@ -1081,9 +1529,9 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1081
1529
  }
1082
1530
 
1083
1531
  private moveToNextCell(rowIndex: number, colId: string, backwards: boolean): void {
1084
- const columns = this.gridApi.getAllColumns().filter(c => c.visible);
1085
- const colIndex = columns.findIndex(c => c.colId === colId);
1086
-
1532
+ const columns = this.gridApi.getAllColumns().filter((c) => c.visible);
1533
+ const colIndex = columns.findIndex((c) => c.colId === colId);
1534
+
1087
1535
  if (colIndex === -1) return;
1088
1536
 
1089
1537
  let nextColIndex = backwards ? colIndex - 1 : colIndex + 1;
@@ -1109,48 +1557,53 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1109
1557
  this.stopEditing(true);
1110
1558
  }
1111
1559
  }
1112
- private getColumnDefForColumn(column: Column | ColDef<TData> | ColGroupDef<TData>): ColDef<TData> | null {
1560
+ private getColumnDefForColumn(
1561
+ column: Column | ColDef<TData> | ColGroupDef<TData>
1562
+ ): ColDef<TData> | null {
1113
1563
  if (!this.columnDefs) return null;
1114
-
1564
+
1115
1565
  const colId = (column as any).colId || (column as any).field?.toString();
1116
1566
  if (!colId) return null;
1117
1567
 
1568
+ const defaultColDef = this.gridOptions?.defaultColDef || {};
1569
+
1118
1570
  for (const def of this.columnDefs) {
1119
1571
  if ('children' in def) {
1120
- const found = def.children.find(c => {
1572
+ const found = def.children.find((c) => {
1121
1573
  const cDef = c as ColDef;
1122
1574
  return cDef.colId === colId || cDef.field?.toString() === colId;
1123
1575
  });
1124
- if (found) return found as ColDef<TData>;
1576
+ if (found) return { ...defaultColDef, ...(found as ColDef<TData>) } as ColDef<TData>;
1125
1577
  } else {
1126
1578
  const cDef = def as ColDef;
1127
1579
  if (cDef.colId === colId || cDef.field?.toString() === colId) {
1128
- return def as ColDef<TData>;
1580
+ return { ...defaultColDef, ...cDef } as ColDef<TData>;
1129
1581
  }
1130
1582
  }
1131
1583
  }
1132
1584
  return null;
1133
1585
  }
1134
1586
 
1135
- // Selection Methods
1136
1587
  onRowClick(rowIndex: number, event: MouseEvent): void {
1137
1588
  const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
1138
1589
  if (!rowNode) return;
1139
1590
 
1140
- // Handle multi-select with Ctrl/Cmd
1141
- if (event.ctrlKey || event.metaKey) {
1142
- rowNode.selected = !rowNode.selected;
1143
- } else if (event.shiftKey) {
1144
- // Range selection (TODO: implement)
1145
- rowNode.selected = true;
1591
+ const selectionMode = this.gridApi.getGridOption('rowSelection') || 'single';
1592
+ const isMultiSelect =
1593
+ (selectionMode as any) === 'multiple' || (selectionMode as any) === 'multiRow';
1594
+
1595
+ if (isMultiSelect && (event.ctrlKey || event.metaKey)) {
1596
+ rowNode.setSelected(!rowNode.selected);
1597
+ } else if (isMultiSelect && event.shiftKey) {
1598
+ rowNode.setSelected(true);
1146
1599
  } else {
1147
- // Single select - deselect all others
1148
- this.gridApi.deselectAll();
1149
- rowNode.selected = true;
1600
+ if (rowNode.selected) {
1601
+ rowNode.setSelected(false);
1602
+ } else {
1603
+ rowNode.setSelected(true, true);
1604
+ }
1150
1605
  }
1151
1606
 
1152
- this.updateSelectionState();
1153
- this.canvasRenderer?.render();
1154
1607
  this.selectionChanged.emit(this.gridApi.getSelectedRows());
1155
1608
  }
1156
1609
 
@@ -1181,7 +1634,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1181
1634
  updateSelectionState(): void {
1182
1635
  const selectedCount = this.gridApi.getSelectedRows().length;
1183
1636
  const totalCount = this.gridApi.getDisplayedRowCount();
1184
-
1637
+
1185
1638
  this.isAllSelected = selectedCount === totalCount && totalCount > 0;
1186
1639
  this.isIndeterminateSelection = selectedCount > 0 && selectedCount < totalCount;
1187
1640
  }