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.
- package/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +2 -2
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +91 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/package.json +20 -6
- package/plan.md +50 -18
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +327 -76
- package/src/lib/components/argent-grid.component.html +186 -64
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +642 -189
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.ts +302 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +366 -0
- package/src/lib/rendering/canvas-renderer.ts +418 -305
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +46 -24
- package/src/lib/rendering/render/column-utils.ts +73 -0
- package/src/lib/rendering/render/hit-test.ts +55 -0
- package/src/lib/rendering/render/index.ts +79 -76
- package/src/lib/rendering/render/lines.ts +43 -43
- package/src/lib/rendering/render/primitives.ts +161 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +2 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +60 -50
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1165 -201
- package/src/lib/services/grid.service.ts +819 -187
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +73 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +188 -0
- package/src/stories/ArgentGrid.stories.ts +277 -0
- package/src/stories/Benchmark.stories.ts +74 -0
- package/src/stories/CellRenderers.stories.ts +221 -0
- package/src/stories/Filtering.stories.ts +252 -0
- package/src/stories/Grouping.stories.ts +217 -0
- package/src/stories/Theming.stories.ts +124 -0
- package/src/stories/benchmark-wrapper.component.ts +315 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
</
|
|
228
|
+
</ng-container>
|
|
157
229
|
</div>
|
|
158
230
|
</div>
|
|
159
231
|
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
160
234
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
</
|
|
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 {
|
|
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 {
|
|
8
|
-
|
|
9
|
-
// Mock
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
94
|
-
component =
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
expect(
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
80
|
+
it('should return separator', () => {
|
|
81
|
+
const item = component.getDefaultMenuItem('separator');
|
|
82
|
+
expect(item?.separator).toBe(true);
|
|
83
|
+
});
|
|
142
84
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
99
|
+
expect(component.activeContextMenu).toBe(false);
|
|
100
|
+
expect(component.contextMenuCell).toBe(null);
|
|
101
|
+
expect(mockCdr.detectChanges).toHaveBeenCalled();
|
|
102
|
+
});
|
|
171
103
|
});
|
|
172
104
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
});
|