argent-grid 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) 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 +70 -27
  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/cell-renderers.spec.ts +152 -0
  28. package/e2e/debug-streaming.spec.ts +31 -0
  29. package/e2e/dnd.spec.ts +73 -0
  30. package/e2e/screenshots.spec.ts +52 -0
  31. package/e2e/theming.spec.ts +35 -0
  32. package/e2e/visual.spec.ts +112 -0
  33. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  37. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  38. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  39. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  40. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  41. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  42. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  43. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  44. package/package.json +21 -7
  45. package/plan.md +56 -28
  46. package/playwright.config.ts +38 -0
  47. package/setup-vitest.ts +10 -13
  48. package/src/lib/argent-grid.module.ts +10 -12
  49. package/src/lib/components/argent-grid.component.css +281 -321
  50. package/src/lib/components/argent-grid.component.html +295 -207
  51. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  52. package/src/lib/components/argent-grid.component.ts +1193 -290
  53. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  54. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  55. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  56. package/src/lib/components/set-filter/set-filter.component.ts +307 -0
  57. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  58. package/src/lib/directives/click-outside.directive.ts +19 -0
  59. package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
  60. package/src/lib/rendering/canvas-renderer.ts +456 -452
  61. package/src/lib/rendering/live-data-handler.ts +110 -0
  62. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  63. package/src/lib/rendering/render/blit.spec.ts +16 -27
  64. package/src/lib/rendering/render/blit.ts +48 -36
  65. package/src/lib/rendering/render/cells.spec.ts +132 -0
  66. package/src/lib/rendering/render/cells.ts +167 -28
  67. package/src/lib/rendering/render/column-utils.ts +95 -0
  68. package/src/lib/rendering/render/hit-test.ts +50 -0
  69. package/src/lib/rendering/render/index.ts +88 -76
  70. package/src/lib/rendering/render/lines.ts +53 -47
  71. package/src/lib/rendering/render/primitives.ts +423 -0
  72. package/src/lib/rendering/render/theme.spec.ts +8 -12
  73. package/src/lib/rendering/render/theme.ts +7 -10
  74. package/src/lib/rendering/render/types.ts +3 -2
  75. package/src/lib/rendering/render/walk.spec.ts +35 -38
  76. package/src/lib/rendering/render/walk.ts +94 -64
  77. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  78. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  79. package/src/lib/rendering/utils/index.ts +1 -1
  80. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  81. package/src/lib/services/grid.service.spec.ts +1241 -201
  82. package/src/lib/services/grid.service.ts +1204 -235
  83. package/src/lib/themes/parts/color-schemes.ts +132 -0
  84. package/src/lib/themes/parts/icon-sets.ts +258 -0
  85. package/src/lib/themes/theme-builder.ts +347 -0
  86. package/src/lib/themes/theme-quartz.ts +72 -0
  87. package/src/lib/themes/types.ts +238 -0
  88. package/src/lib/types/ag-grid-types.ts +573 -14
  89. package/src/public-api.ts +39 -9
  90. package/src/stories/Advanced.stories.ts +249 -0
  91. package/src/stories/ArgentGrid.stories.ts +301 -0
  92. package/src/stories/Benchmark.stories.ts +76 -0
  93. package/src/stories/CellRenderers.stories.ts +395 -0
  94. package/src/stories/Filtering.stories.ts +292 -0
  95. package/src/stories/Grouping.stories.ts +290 -0
  96. package/src/stories/Streaming.stories.ts +57 -0
  97. package/src/stories/Theming.stories.ts +137 -0
  98. package/src/stories/Tooltips.stories.ts +381 -0
  99. package/src/stories/benchmark-wrapper.component.ts +355 -0
  100. package/src/stories/story-utils.ts +88 -0
  101. package/src/stories/streaming-wrapper.component.ts +441 -0
  102. package/tsconfig.json +1 -0
  103. package/tsconfig.storybook.json +10 -0
  104. package/vitest.config.ts +9 -9
  105. package/demo-app/README.md +0 -70
  106. package/demo-app/angular.json +0 -78
  107. package/demo-app/e2e/benchmark.spec.ts +0 -53
  108. package/demo-app/e2e/demo-page.spec.ts +0 -77
  109. package/demo-app/e2e/grid-features.spec.ts +0 -269
  110. package/demo-app/package-lock.json +0 -14023
  111. package/demo-app/package.json +0 -36
  112. package/demo-app/playwright-test-menu.js +0 -19
  113. package/demo-app/playwright.config.ts +0 -23
  114. package/demo-app/src/app/app.component.ts +0 -10
  115. package/demo-app/src/app/app.config.ts +0 -13
  116. package/demo-app/src/app/app.routes.ts +0 -7
  117. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  118. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  119. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  120. package/demo-app/src/index.html +0 -19
  121. package/demo-app/src/main.ts +0 -6
  122. package/demo-app/tsconfig.json +0 -31
@@ -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
  });