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
@@ -5,17 +5,7 @@
5
5
  <div class="argent-grid-header">
6
6
  <!-- Main Header Row -->
7
7
  <div class="argent-grid-header-row">
8
- <!-- Selection Column Header -->
9
- <div
10
- *ngIf="showSelectionColumn"
11
- class="argent-grid-header-cell argent-grid-selection-header"
12
- [style.width.px]="selectionColumnWidth"
13
- (click)="onSelectionHeaderClick()">
14
- <input type="checkbox"
15
- [checked]="isAllSelected"
16
- [indeterminate]="isIndeterminateSelection"
17
- (change)="onSelectionHeaderChange($event)" />
18
- </div>
8
+
19
9
 
20
10
  <!-- Left Pinned Columns -->
21
11
  <div class="argent-grid-header-pinned-left-container"
@@ -27,12 +17,21 @@
27
17
  <div
28
18
  *ngFor="let col of getLeftPinnedColumns(); trackBy: trackByColumn"
29
19
  class="argent-grid-header-cell argent-grid-header-cell-pinned-left"
20
+ [class.center-content]="col.colId === 'ag-Grid-SelectionColumn'"
30
21
  [style.width.px]="getColumnWidth(col)"
31
22
  [class.sortable]="isSortable(col)"
32
23
  (click)="onHeaderClick(col)"
33
24
  cdkDrag
25
+ [cdkDragDisabled]="col.colId === 'ag-Grid-SelectionColumn'"
34
26
  [cdkDragData]="col">
35
27
  <div class="argent-grid-header-content" cdkDragHandle>
28
+ <div *ngIf="hasHeaderCheckbox(col)" class="argent-grid-header-checkbox">
29
+ <input type="checkbox"
30
+ [checked]="isAllSelected"
31
+ [indeterminate]="isIndeterminateSelection"
32
+ (click)="$event.stopPropagation()"
33
+ (change)="onSelectionHeaderChange($event)" />
34
+ </div>
36
35
  <span class="header-text">{{ getHeaderName(col) }}</span>
37
36
  <span class="sort-indicator" *ngIf="getSortIndicator(col)">{{ getSortIndicator(col) }}</span>
38
37
  </div>
@@ -59,12 +58,21 @@
59
58
  <div
60
59
  *ngFor="let col of getNonPinnedColumns(); trackBy: trackByColumn"
61
60
  class="argent-grid-header-cell"
61
+ [class.center-content]="col.colId === 'ag-Grid-SelectionColumn'"
62
62
  [style.width.px]="getColumnWidth(col)"
63
63
  [class.sortable]="isSortable(col)"
64
64
  (click)="onHeaderClick(col)"
65
65
  cdkDrag
66
+ [cdkDragDisabled]="col.colId === 'ag-Grid-SelectionColumn'"
66
67
  [cdkDragData]="col">
67
68
  <div class="argent-grid-header-content" cdkDragHandle>
69
+ <div *ngIf="hasHeaderCheckbox(col)" class="argent-grid-header-checkbox">
70
+ <input type="checkbox"
71
+ [checked]="isAllSelected"
72
+ [indeterminate]="isIndeterminateSelection"
73
+ (click)="$event.stopPropagation()"
74
+ (change)="onSelectionHeaderChange($event)" />
75
+ </div>
68
76
  <span class="header-text">{{ getHeaderName(col) }}</span>
69
77
  <span class="sort-indicator" *ngIf="getSortIndicator(col)">{{ getSortIndicator(col) }}</span>
70
78
  </div>
@@ -90,12 +98,21 @@
90
98
  <div
91
99
  *ngFor="let col of getRightPinnedColumns(); trackBy: trackByColumn"
92
100
  class="argent-grid-header-cell argent-grid-header-cell-pinned-right"
101
+ [class.center-content]="col.colId === 'ag-Grid-SelectionColumn'"
93
102
  [style.width.px]="getColumnWidth(col)"
94
103
  [class.sortable]="isSortable(col)"
95
104
  (click)="onHeaderClick(col)"
96
105
  cdkDrag
106
+ [cdkDragDisabled]="col.colId === 'ag-Grid-SelectionColumn'"
97
107
  [cdkDragData]="col">
98
108
  <div class="argent-grid-header-content" cdkDragHandle>
109
+ <div *ngIf="hasHeaderCheckbox(col)" class="argent-grid-header-checkbox">
110
+ <input type="checkbox"
111
+ [checked]="isAllSelected"
112
+ [indeterminate]="isIndeterminateSelection"
113
+ (click)="$event.stopPropagation()"
114
+ (change)="onSelectionHeaderChange($event)" />
115
+ </div>
99
116
  <span class="header-text">{{ getHeaderName(col) }}</span>
100
117
  <span class="sort-indicator" *ngIf="getSortIndicator(col)">{{ getSortIndicator(col) }}</span>
101
118
  </div>
@@ -113,25 +130,36 @@
113
130
 
114
131
  <!-- Floating Filter Row -->
115
132
  <div class="argent-grid-header-row floating-filter-row" *ngIf="hasFloatingFilters()">
116
- <!-- Selection Column Padding -->
117
- <div *ngIf="showSelectionColumn" class="argent-grid-header-cell" [style.width.px]="selectionColumnWidth"></div>
133
+
118
134
 
119
135
  <!-- Left Pinned Filters -->
120
136
  <div class="argent-grid-header-pinned-left-container">
121
137
  <div
122
138
  *ngFor="let col of getLeftPinnedColumns(); trackBy: trackByColumn"
123
- class="argent-grid-header-cell argent-grid-header-cell-pinned-left"
139
+ class="argent-grid-header-cell argent-grid-header-cell-pinned-left argent-grid-floating-filter-cell"
140
+ [class.center-content]="col.colId === 'ag-Grid-SelectionColumn'"
124
141
  [style.width.px]="getColumnWidth(col)">
125
142
  <div class="floating-filter-container" *ngIf="isFloatingFilterEnabled(col)">
126
- <input #filterInput
127
- class="floating-filter-input"
128
- [type]="getFilterInputType(col)"
129
- [value]="getFloatingFilterValue(col)"
130
- (input)="onFloatingFilterInput($event, col)"
131
- [placeholder]="'Filter...'" />
132
- <span class="floating-filter-clear"
133
- *ngIf="hasFilterValue(col, filterInput)"
134
- (click)="clearFloatingFilter(col, filterInput)">✕</span>
143
+ <!-- Set Filter Button -->
144
+ <button *ngIf="isSetFilter(col)"
145
+ type="button"
146
+ class="floating-filter-btn"
147
+ (click)="openSetFilter($event, col)"
148
+ [class.active]="hasSetFilterValue(col)">
149
+ 🔽 {{ getSetFilterCount(col) }}
150
+ </button>
151
+ <!-- Text/Number/Date Filter Input -->
152
+ <ng-container *ngIf="!isSetFilter(col)">
153
+ <input #filterInput
154
+ class="floating-filter-input"
155
+ [type]="getFilterInputType(col)"
156
+ [value]="getFloatingFilterValue(col)"
157
+ (input)="onFloatingFilterInput($event, col)"
158
+ [placeholder]="'Filter...'" />
159
+ <span class="floating-filter-clear"
160
+ *ngIf="hasFilterValue(col, filterInput)"
161
+ (click)="clearFloatingFilter(col, filterInput)">✕</span>
162
+ </ng-container>
135
163
  </div>
136
164
  </div>
137
165
  </div>
@@ -141,9 +169,53 @@
141
169
  <div class="argent-grid-header-row">
142
170
  <div
143
171
  *ngFor="let col of getNonPinnedColumns(); trackBy: trackByColumn"
144
- class="argent-grid-header-cell"
172
+ class="argent-grid-header-cell argent-grid-floating-filter-cell"
173
+ [class.center-content]="col.colId === 'ag-Grid-SelectionColumn'"
145
174
  [style.width.px]="getColumnWidth(col)">
146
175
  <div class="floating-filter-container" *ngIf="isFloatingFilterEnabled(col)">
176
+ <!-- Set Filter Button -->
177
+ <button *ngIf="isSetFilter(col)"
178
+ type="button"
179
+ class="floating-filter-btn"
180
+ (click)="openSetFilter($event, col)"
181
+ [class.active]="hasSetFilterValue(col)">
182
+ 🔽 {{ getSetFilterCount(col) }}
183
+ </button>
184
+ <!-- Text/Number/Date Filter Input -->
185
+ <ng-container *ngIf="!isSetFilter(col)">
186
+ <input #filterInput
187
+ class="floating-filter-input"
188
+ [type]="getFilterInputType(col)"
189
+ [value]="getFloatingFilterValue(col)"
190
+ (input)="onFloatingFilterInput($event, col)"
191
+ [placeholder]="'Filter...'" />
192
+ <span class="floating-filter-clear"
193
+ *ngIf="hasFilterValue(col, filterInput)"
194
+ (click)="clearFloatingFilter(col, filterInput)">✕</span>
195
+ </ng-container>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Right Pinned Filters -->
202
+ <div class="argent-grid-header-pinned-right-container">
203
+ <div
204
+ *ngFor="let col of getRightPinnedColumns(); trackBy: trackByColumn"
205
+ class="argent-grid-header-cell argent-grid-header-cell-pinned-right argent-grid-floating-filter-cell"
206
+ [class.center-content]="col.colId === 'ag-Grid-SelectionColumn'"
207
+ [style.width.px]="getColumnWidth(col)">
208
+ <div class="floating-filter-container" *ngIf="isFloatingFilterEnabled(col)">
209
+ <!-- Set Filter Button -->
210
+ <button *ngIf="isSetFilter(col)"
211
+ type="button"
212
+ class="floating-filter-btn"
213
+ (click)="openSetFilter($event, col)"
214
+ [class.active]="hasSetFilterValue(col)">
215
+ 🔽 {{ getSetFilterCount(col) }}
216
+ </button>
217
+ <!-- Text/Number/Date Filter Input -->
218
+ <ng-container *ngIf="!isSetFilter(col)">
147
219
  <input #filterInput
148
220
  class="floating-filter-input"
149
221
  [type]="getFilterInputType(col)"
@@ -153,29 +225,79 @@
153
225
  <span class="floating-filter-clear"
154
226
  *ngIf="hasFilterValue(col, filterInput)"
155
227
  (click)="clearFloatingFilter(col, filterInput)">✕</span>
156
- </div>
228
+ </ng-container>
157
229
  </div>
158
230
  </div>
159
231
  </div>
232
+ </div>
233
+ </div>
160
234
 
161
- <!-- Right Pinned Filters -->
162
- <div class="argent-grid-header-pinned-right-container">
163
- <div
164
- *ngFor="let col of getRightPinnedColumns(); trackBy: trackByColumn"
165
- class="argent-grid-header-cell argent-grid-header-cell-pinned-right"
166
- [style.width.px]="getColumnWidth(col)">
167
- <div class="floating-filter-container" *ngIf="isFloatingFilterEnabled(col)">
168
- <input #filterInput
169
- class="floating-filter-input"
170
- [type]="getFilterInputType(col)"
171
- [value]="getFloatingFilterValue(col)"
172
- (input)="onFloatingFilterInput($event, col)"
173
- [placeholder]="'Filter...'" />
174
- <span class="floating-filter-clear"
175
- *ngIf="hasFilterValue(col, filterInput)"
176
- (click)="clearFloatingFilter(col, filterInput)">✕</span>
177
- </div>
235
+ <!-- Set Filter Popup -->
236
+ <div *ngIf="activeSetFilter"
237
+ class="set-filter-popup"
238
+ [style.left.px]="setFilterPosition.x"
239
+ [style.top.px]="setFilterPosition.y"
240
+ (clickOutside)="closeSetFilter()"
241
+ clickOutside>
242
+ <argent-set-filter
243
+ [values]="setFilterValues"
244
+ [valueFormatter]="setFilterValueFormatter"
245
+ (filterChanged)="onSetFilterChanged($event)">
246
+ </argent-set-filter>
247
+ </div>
248
+
249
+ <!-- Text/Number Filter Popup -->
250
+ <div *ngIf="activeFilterPopup"
251
+ class="filter-popup"
252
+ [style.left.px]="filterPopupPosition.x"
253
+ [style.top.px]="filterPopupPosition.y"
254
+ (clickOutside)="closeFilterPopup()"
255
+ clickOutside>
256
+ <div class="filter-popup-header">
257
+ <span>Filter {{ getHeaderName(activeFilterPopupColumn!) }}</span>
258
+ <span class="filter-popup-close" (click)="closeFilterPopup()">✕</span>
259
+ </div>
260
+ <div class="filter-popup-body">
261
+ <!-- Operator Select -->
262
+ <div class="filter-popup-row">
263
+ <select class="filter-popup-select"
264
+ [value]="activeFilterOperator"
265
+ (change)="onFilterPopupOperatorChange($any($event.target).value)">
266
+ <ng-container *ngIf="activeFilterPopupType === 'text'">
267
+ <option *ngFor="let op of textFilterOperators" [value]="op.value">{{ op.label }}</option>
268
+ </ng-container>
269
+ <ng-container *ngIf="activeFilterPopupType === 'number'">
270
+ <option *ngFor="let op of numberFilterOperators" [value]="op.value">{{ op.label }}</option>
271
+ </ng-container>
272
+ </select>
273
+ </div>
274
+
275
+ <!-- Value Input(s) -->
276
+ <ng-container *ngIf="activeFilterOperator !== 'blank' && activeFilterOperator !== 'notBlank'">
277
+ <div class="filter-popup-row">
278
+ <input #filterPopupInput
279
+ [type]="activeFilterPopupType === 'number' ? 'number' : 'text'"
280
+ class="filter-popup-input"
281
+ [value]="filterValue1"
282
+ (input)="onFilterPopupInput($event)"
283
+ (keydown.enter)="closeFilterPopup()"
284
+ [placeholder]="activeFilterOperator === 'inRange' ? 'From...' : 'Filter value...'"
285
+ autofocus />
286
+ </div>
287
+
288
+ <div class="filter-popup-row" *ngIf="activeFilterOperator === 'inRange'">
289
+ <input type="number"
290
+ class="filter-popup-input"
291
+ [value]="filterValue2"
292
+ (input)="onFilterPopupInput($event, true)"
293
+ (keydown.enter)="closeFilterPopup()"
294
+ [placeholder]="'To...'" />
178
295
  </div>
296
+ </ng-container>
297
+
298
+ <div class="filter-popup-footer">
299
+ <button class="filter-popup-btn clear-btn" (click)="clearColumnFilter(activeFilterPopupColumn!)">Clear</button>
300
+ <button class="filter-popup-btn apply-btn" (click)="closeFilterPopup()">Apply</button>
179
301
  </div>
180
302
  </div>
181
303
  </div>
@@ -264,28 +386,28 @@
264
386
  [style.top.px]="headerMenuPosition.y"
265
387
  [style.left.px]="headerMenuPosition.x"
266
388
  (click)="$event.stopPropagation()">
267
- <div class="menu-item" (click)="sortColumnMenu('asc')">
268
- <span class="menu-icon">↑</span> Sort Ascending
269
- </div>
270
- <div class="menu-item" (click)="sortColumnMenu('desc')">
271
- <span class="menu-icon">↓</span> Sort Descending
272
- </div>
273
- <div class="menu-item" (click)="sortColumnMenu(null)">
274
- <span class="menu-icon">✕</span> Clear Sort
275
- </div>
276
- <div class="menu-divider"></div>
277
- <div class="menu-item" (click)="hideColumnMenu()">
278
- <span class="menu-icon">ø</span> Hide Column
279
- </div>
280
- <div class="menu-item" (click)="pinColumnMenu('left')">
281
- <span class="menu-icon">«</span> Pin Left
282
- </div>
283
- <div class="menu-item" (click)="pinColumnMenu('right')">
284
- <span class="menu-icon">»</span> Pin Right
285
- </div>
286
- <div class="menu-item" (click)="pinColumnMenu(null)">
287
- <span class="menu-icon">↺</span> Unpin
288
- </div>
389
+ <ng-container *ngFor="let item of headerMenuItems">
390
+ <div *ngIf="item.separator" class="menu-divider"></div>
391
+ <div *ngIf="!item.separator"
392
+ class="menu-item"
393
+ [class.disabled]="item.disabled"
394
+ [class.has-submenu]="item.subMenu && item.subMenu.length > 0"
395
+ (click)="!item.disabled && item.action(); !item.subMenu && closeHeaderMenu()">
396
+ <span class="menu-icon" *ngIf="item.icon">{{ item.icon }}</span>
397
+ <span class="menu-text">{{ item.name }}</span>
398
+ <span class="menu-arrow" *ngIf="item.subMenu && item.subMenu.length > 0">▶</span>
399
+
400
+ <!-- Sub-menu -->
401
+ <div class="argent-grid-header-menu sub-menu" *ngIf="item.subMenu && item.subMenu.length > 0">
402
+ <div *ngFor="let subItem of item.subMenu"
403
+ class="menu-item"
404
+ (click)="subItem.action(); closeHeaderMenu(); $event.stopPropagation()">
405
+ <span class="menu-icon" *ngIf="subItem.icon">{{ subItem.icon }}</span>
406
+ <span class="menu-text">{{ subItem.name }}</span>
407
+ </div>
408
+ </div>
409
+ </div>
410
+ </ng-container>
289
411
  </div>
290
412
 
291
413
  <!-- Context Menu Overlay -->
@@ -1,189 +1,149 @@
1
- import { TestBed, ComponentFixture } from '@angular/core/testing';
2
- import { ChangeDetectorRef, provideExperimentalZonelessChangeDetection } from '@angular/core';
3
- import { CommonModule } from '@angular/common';
4
- import { DragDropModule } from '@angular/cdk/drag-drop';
5
- import { ArgentGridComponent } from './argent-grid.component';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
2
  import { GridService } from '../services/grid.service';
7
- import { ColDef } from '../types/ag-grid-types';
8
-
9
- // Mock canvas context
10
- const mockCanvasContext = {
11
- clearRect: vi.fn(),
12
- fillRect: vi.fn(),
13
- beginPath: vi.fn(),
14
- moveTo: vi.fn(),
15
- lineTo: vi.fn(),
16
- stroke: vi.fn(),
17
- fillText: vi.fn(),
18
- measureText: vi.fn(() => ({ width: 100 })),
19
- scale: vi.fn(),
20
- setTransform: vi.fn(),
21
- font: '13px sans-serif',
22
- textBaseline: 'middle',
23
- fillStyle: '#000',
24
- strokeStyle: '#000'
3
+ import { ArgentGridComponent } from './argent-grid.component';
4
+
5
+ // Mock ChangeDetectorRef
6
+ const mockCdr = {
7
+ detectChanges: vi.fn(),
8
+ markForCheck: vi.fn(),
25
9
  };
26
10
 
27
- const mockCanvas = {
28
- getContext: vi.fn(() => mockCanvasContext as any),
29
- width: 800,
30
- height: 600,
31
- style: {},
32
- addEventListener: vi.fn(),
33
- getBoundingClientRect: vi.fn(() => ({ width: 800, height: 600 }))
34
- } as unknown as HTMLCanvasElement;
35
-
36
- interface TestData {
37
- id: number;
38
- name: string;
39
- value: number;
40
- }
41
-
42
- describe('ArgentGridComponent', () => {
43
- let component: ArgentGridComponent<TestData>;
44
- let fixture: ComponentFixture<ArgentGridComponent<TestData>>;
45
-
46
- // Mock getContext globally for this test suite
47
- beforeAll(() => {
48
- vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation((contextId) => {
49
- if (contextId === '2d') {
50
- return mockCanvasContext as any;
51
- }
52
- return null;
53
- });
54
-
55
- vi.spyOn(HTMLCanvasElement.prototype, 'getBoundingClientRect').mockReturnValue({
56
- width: 800,
57
- height: 600,
58
- top: 0,
59
- left: 0,
60
- bottom: 600,
61
- right: 800,
62
- x: 0,
63
- y: 0,
64
- toJSON: () => {}
65
- } as DOMRect);
66
- });
67
-
68
- const testColumnDefs: (ColDef<TestData>)[] = [
69
- { colId: 'id', field: 'id', headerName: 'ID', width: 100 },
70
- { colId: 'name', field: 'name', headerName: 'Name', width: 150 },
71
- { colId: 'value', field: 'value', headerName: 'Value', width: 100 }
72
- ];
73
-
74
- const testRowData: TestData[] = [
75
- { id: 1, name: 'Item 1', value: 100 },
76
- { id: 2, name: 'Item 2', value: 200 }
77
- ];
78
-
79
- beforeEach(async () => {
80
- await TestBed.configureTestingModule({
81
- declarations: [ArgentGridComponent],
82
- imports: [
83
- CommonModule,
84
- DragDropModule
85
- ],
86
- providers: [
87
- provideExperimentalZonelessChangeDetection()
88
- ]
89
- }).compileComponents();
90
- });
11
+ describe('ArgentGridComponent - Context Menu', () => {
12
+ let component: ArgentGridComponent;
13
+ let _gridService: GridService;
91
14
 
92
15
  beforeEach(() => {
93
- fixture = TestBed.createComponent(ArgentGridComponent<TestData>) as ComponentFixture<ArgentGridComponent<TestData>>;
94
- component = fixture.componentInstance;
95
- component.columnDefs = testColumnDefs;
96
- component.rowData = testRowData;
97
-
98
- fixture.detectChanges();
16
+ _gridService = new GridService();
17
+ component = new ArgentGridComponent(mockCdr as any);
99
18
  });
100
19
 
101
- it('should create', () => {
102
- expect(component).toBeTruthy();
103
- });
20
+ describe('resolveContextMenuItems', () => {
21
+ it('should resolve string menu items to defaults', () => {
22
+ const items = component.resolveContextMenuItems(['copy', 'separator']);
23
+ expect(items.length).toBeGreaterThan(0);
24
+ expect(items[0].name).toBe('Copy Cell');
25
+ });
104
26
 
105
- it('should accept columnDefs input', () => {
106
- expect(component.columnDefs).toEqual(testColumnDefs);
107
- });
27
+ it('should handle custom MenuItemDef objects', () => {
28
+ const customItem = {
29
+ name: 'Custom Action',
30
+ action: vi.fn(),
31
+ icon: '⭐',
32
+ };
33
+ const items = component.resolveContextMenuItems([customItem]);
34
+ expect(items.length).toBe(1);
35
+ expect(items[0].name).toBe('Custom Action');
36
+ });
108
37
 
109
- it('should accept rowData input', () => {
110
- expect(component.rowData).toEqual(testRowData);
111
- });
38
+ it('should mix default and custom items', () => {
39
+ const items = component.resolveContextMenuItems([
40
+ 'copy',
41
+ { name: 'Custom', action: vi.fn() },
42
+ 'separator',
43
+ ]);
44
+ expect(items.length).toBeGreaterThan(1);
45
+ });
112
46
 
113
- it('should emit gridReady event', () => {
114
- component.gridReady.subscribe((api) => {
115
- expect(api).toBeTruthy();
116
- expect(api.getColumnDefs()).toEqual(testColumnDefs);
47
+ it('should filter out null items', () => {
48
+ const items = component.resolveContextMenuItems(['copy', null as any]);
49
+ expect(items.length).toBeGreaterThan(0);
117
50
  });
118
51
  });
119
52
 
120
- it('should have correct row height', () => {
121
- expect(component.rowHeight).toBe(32);
122
- });
53
+ describe('getDefaultMenuItem', () => {
54
+ it('should return copy cell item', () => {
55
+ const item = component.getDefaultMenuItem('copy');
56
+ expect(item?.name).toBe('Copy Cell');
57
+ expect(item?.icon).toBe('📋');
58
+ });
123
59
 
124
- it('should have viewport for virtual scrolling', () => {
125
- // Virtual scrolling is now handled by the viewport container
126
- expect(component.viewportRef).toBeDefined();
127
- });
60
+ it('should return copy with headers item (when range exists)', () => {
61
+ // Note: copyWithHeaders only returns item when range selection exists
62
+ // For testing, we just verify it doesn't throw
63
+ expect(() => {
64
+ component.getDefaultMenuItem('copyWithHeaders');
65
+ }).not.toThrow();
66
+ });
128
67
 
129
- it('should show overlay when no data', () => {
130
- // Test the logic without calling ngOnInit which triggers canvas
131
- expect(component.showOverlay).toBe(false); // Initially has data
132
- });
68
+ it('should return export submenu', () => {
69
+ const item = component.getDefaultMenuItem('export');
70
+ expect(item?.name).toBe('Export');
71
+ expect(item?.subMenu).toBeDefined();
72
+ });
133
73
 
134
- it('should hide overlay when data exists', () => {
135
- expect(component.showOverlay).toBe(false);
136
- });
74
+ it('should return reset columns item', () => {
75
+ const item = component.getDefaultMenuItem('resetColumns');
76
+ expect(item?.name).toBe('Reset Columns');
77
+ expect(item?.icon).toBe('⟲');
78
+ });
137
79
 
138
- it('should get header name from column', () => {
139
- const col = testColumnDefs[0];
140
- expect(component.getHeaderName(col)).toBe('ID');
141
- });
80
+ it('should return separator', () => {
81
+ const item = component.getDefaultMenuItem('separator');
82
+ expect(item?.separator).toBe(true);
83
+ });
142
84
 
143
- it('should get header name from field if headerName not provided', () => {
144
- const col: ColDef<TestData> = { field: 'name' };
145
- expect(component.getHeaderName(col)).toBe('name');
85
+ it('should return null for unknown key', () => {
86
+ const item = component.getDefaultMenuItem('unknown' as any);
87
+ expect(item).toBe(null);
88
+ });
146
89
  });
147
90
 
148
- it('should handle header click for sorting', () => {
149
- const col = { ...testColumnDefs[0], sortable: true };
150
- component.onHeaderClick(col);
151
- expect(col.sort).toBe('asc');
152
-
153
- // Click again to toggle
154
- component.onHeaderClick(col);
155
- expect(col.sort).toBe('desc');
156
-
157
- // Click third time to clear
158
- component.onHeaderClick(col);
159
- expect(col.sort).toBeNull();
160
- });
91
+ describe('closeContextMenu', () => {
92
+ it('should reset context menu state', () => {
93
+ component.contextMenuItems = [{ name: 'Test', action: vi.fn() }];
94
+ component.activeContextMenu = true;
95
+ component.contextMenuCell = { rowNode: {} as any, column: {} as any };
161
96
 
162
- it('should not sort non-sortable columns', () => {
163
- const col: ColDef<TestData> = { colId: 'test', sortable: false };
164
- component.onHeaderClick(col);
165
- expect(col.sort).toBeUndefined();
166
- });
97
+ component.closeContextMenu();
167
98
 
168
- it('should get column width', () => {
169
- const col = testColumnDefs[0];
170
- expect(component.getColumnWidth(col)).toBe(100);
99
+ expect(component.activeContextMenu).toBe(false);
100
+ expect(component.contextMenuCell).toBe(null);
101
+ expect(mockCdr.detectChanges).toHaveBeenCalled();
102
+ });
171
103
  });
172
104
 
173
- it('should use default width if not specified', () => {
174
- const col: ColDef<TestData> = { colId: 'test' };
175
- expect(component.getColumnWidth(col)).toBe(150);
176
- });
105
+ describe('copyContextMenuCell', () => {
106
+ it('should handle null cell gracefully', () => {
107
+ component.contextMenuCell = null;
108
+ expect(() => component.copyContextMenuCell()).not.toThrow();
109
+ });
177
110
 
178
- it('should refresh grid', () => {
179
- const mockRenderer = { render: vi.fn(), destroy: vi.fn() };
180
- (component as any).canvasRenderer = mockRenderer;
181
- component.refresh();
182
- expect(mockRenderer.render).toHaveBeenCalled();
111
+ it('should handle missing field gracefully', () => {
112
+ component.contextMenuCell = {
113
+ rowNode: { data: { name: 'John' } } as any,
114
+ column: { field: null } as any,
115
+ };
116
+ expect(() => component.copyContextMenuCell()).not.toThrow();
117
+ });
118
+
119
+ it('should close context menu after copy', () => {
120
+ const mockClipboard = { writeText: vi.fn().mockResolvedValue(undefined) };
121
+ Object.defineProperty(navigator, 'clipboard', { value: mockClipboard, writable: true });
122
+
123
+ component.contextMenuCell = {
124
+ rowNode: { data: { name: 'John' } } as any,
125
+ column: { field: 'name' } as any,
126
+ };
127
+ component.activeContextMenu = true;
128
+
129
+ component.copyContextMenuCell();
130
+
131
+ expect(component.activeContextMenu).toBe(false);
132
+ expect(component.contextMenuCell).toBe(null);
133
+ });
183
134
  });
184
135
 
185
- it('should get API instance', () => {
186
- const api = component.getApi();
187
- expect(api).toBeTruthy();
136
+ describe('hasRangeSelection', () => {
137
+ it('should return false when no range', () => {
138
+ const mockApi = { getCellRanges: vi.fn(() => []) };
139
+ component.gridApi = mockApi as any;
140
+ expect(component.hasRangeSelection()).toBe(false);
141
+ });
142
+
143
+ it('should return true when range exists', () => {
144
+ const mockApi = { getCellRanges: vi.fn(() => [{}]) };
145
+ component.gridApi = mockApi as any;
146
+ expect(component.hasRangeSelection()).toBe(true);
147
+ });
188
148
  });
189
149
  });