@dragonworks/ngx-dashboard 20.0.5 → 20.0.6

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 (78) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +34 -45
  3. package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +537 -0
  4. package/src/lib/cell/__tests__/cell-resize.component.spec.ts +442 -0
  5. package/src/lib/cell/__tests__/cell.component.spec.ts +541 -0
  6. package/src/lib/cell/cell-context-menu.component.ts +138 -0
  7. package/src/lib/cell/cell-context-menu.service.ts +36 -0
  8. package/src/lib/cell/cell.component.html +37 -0
  9. package/src/lib/cell/cell.component.scss +198 -0
  10. package/src/lib/cell/cell.component.ts +375 -0
  11. package/src/lib/dashboard/dashboard.component.html +18 -0
  12. package/src/lib/dashboard/dashboard.component.scss +17 -0
  13. package/src/lib/dashboard/dashboard.component.ts +187 -0
  14. package/src/lib/dashboard-editor/dashboard-editor.component.html +57 -0
  15. package/src/lib/dashboard-editor/dashboard-editor.component.scss +87 -0
  16. package/src/lib/dashboard-editor/dashboard-editor.component.ts +219 -0
  17. package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +258 -0
  18. package/src/lib/dashboard-viewer/dashboard-viewer.component.html +20 -0
  19. package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +50 -0
  20. package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +70 -0
  21. package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +465 -0
  22. package/src/lib/drop-zone/drop-zone.component.html +20 -0
  23. package/src/lib/drop-zone/drop-zone.component.scss +67 -0
  24. package/src/lib/drop-zone/drop-zone.component.ts +122 -0
  25. package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +72 -0
  26. package/src/lib/models/cell-data.ts +13 -0
  27. package/src/lib/models/cell-dialog.ts +7 -0
  28. package/src/lib/models/cell-id.ts +85 -0
  29. package/src/lib/models/cell-position.ts +15 -0
  30. package/src/lib/models/dashboard-data.dto.ts +44 -0
  31. package/src/lib/models/dashboard-data.utils.ts +49 -0
  32. package/src/lib/models/drag-data.ts +6 -0
  33. package/src/lib/models/index.ts +11 -0
  34. package/src/lib/models/reserved-space.ts +24 -0
  35. package/src/lib/models/widget-factory.ts +33 -0
  36. package/src/lib/models/widget-id.ts +70 -0
  37. package/src/lib/models/widget.ts +21 -0
  38. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +127 -0
  39. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +15 -0
  40. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +20 -0
  41. package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +32 -0
  42. package/src/lib/providers/cell-settings-dialog/index.ts +3 -0
  43. package/src/lib/providers/index.ts +1 -0
  44. package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +220 -0
  45. package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +362 -0
  46. package/src/lib/services/dashboard-bridge.service.ts +155 -0
  47. package/src/lib/services/dashboard-viewport.service.ts +148 -0
  48. package/src/lib/services/dashboard.service.ts +62 -0
  49. package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +756 -0
  50. package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +974 -0
  51. package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +279 -0
  52. package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +780 -0
  53. package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +128 -0
  54. package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +229 -0
  55. package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +652 -0
  56. package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +461 -0
  57. package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +369 -0
  58. package/src/lib/store/dashboard-store.ts +239 -0
  59. package/src/lib/store/features/drag-drop.feature.ts +140 -0
  60. package/src/lib/store/features/grid-config.feature.ts +43 -0
  61. package/src/lib/store/features/resize.feature.ts +140 -0
  62. package/src/lib/store/features/utils/collision.utils.ts +89 -0
  63. package/src/lib/store/features/utils/grid-query-internal.utils.ts +37 -0
  64. package/src/lib/store/features/utils/resize.utils.ts +165 -0
  65. package/src/lib/store/features/widget-management.feature.ts +158 -0
  66. package/src/lib/styles/_dashboard-grid-vars.scss +11 -0
  67. package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +137 -0
  68. package/src/lib/widget-list/widget-list.component.html +22 -0
  69. package/src/lib/widget-list/widget-list.component.scss +154 -0
  70. package/src/lib/widget-list/widget-list.component.ts +106 -0
  71. package/src/public-api.ts +21 -0
  72. package/src/test-setup.ts +10 -0
  73. package/tsconfig.lib.json +15 -0
  74. package/tsconfig.lib.prod.json +11 -0
  75. package/tsconfig.spec.json +14 -0
  76. package/fesm2022/dragonworks-ngx-dashboard.mjs +0 -2178
  77. package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
  78. package/index.d.ts +0 -678
@@ -0,0 +1,780 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { DashboardService } from '../../services/dashboard.service';
3
+ import { DashboardStore } from '../dashboard-store';
4
+ import { CellIdUtils, WidgetIdUtils, CellData, WidgetFactory, DashboardDataDto, Widget } from '../../models';
5
+
6
+ describe('DashboardStore - Export/Import Functionality', () => {
7
+ let store: InstanceType<typeof DashboardStore>;
8
+ let mockWidgetFactory: WidgetFactory;
9
+ let mockDashboardService: jasmine.SpyObj<DashboardService>;
10
+
11
+ beforeEach(() => {
12
+ const dashboardServiceSpy = jasmine.createSpyObj('DashboardService', ['getFactory']);
13
+
14
+ TestBed.configureTestingModule({
15
+ providers: [
16
+ DashboardStore,
17
+ { provide: DashboardService, useValue: dashboardServiceSpy }
18
+ ]
19
+ });
20
+
21
+ store = TestBed.inject(DashboardStore);
22
+ mockDashboardService = TestBed.inject(DashboardService) as jasmine.SpyObj<DashboardService>;
23
+ store.setGridConfig({ rows: 8, columns: 12 });
24
+
25
+ mockWidgetFactory = {
26
+ widgetTypeid: 'test-widget',
27
+ createComponent: jasmine.createSpy()
28
+ } as any;
29
+
30
+ mockDashboardService.getFactory.and.returnValue(mockWidgetFactory);
31
+ });
32
+
33
+ describe('exportDashboard', () => {
34
+ it('should export empty dashboard', () => {
35
+ const exported = store.exportDashboard();
36
+
37
+ expect(exported).toEqual({
38
+ version: '1.0.0',
39
+ dashboardId: '',
40
+ rows: 8,
41
+ columns: 12,
42
+ gutterSize: '0.5em',
43
+ cells: []
44
+ });
45
+ });
46
+
47
+ it('should export dashboard with single widget', () => {
48
+ const cell: CellData = {
49
+ widgetId: WidgetIdUtils.generate(),
50
+ cellId: CellIdUtils.create(3, 4),
51
+ row: 3,
52
+ col: 4,
53
+ rowSpan: 2,
54
+ colSpan: 3,
55
+ widgetFactory: mockWidgetFactory,
56
+ widgetState: { color: 'red' },
57
+ flat: true,
58
+ };
59
+
60
+ store.addWidget(cell);
61
+ const exported = store.exportDashboard();
62
+
63
+ expect(exported.cells.length).toBe(1);
64
+ expect(exported.cells[0]).toEqual({
65
+ row: 3,
66
+ col: 4,
67
+ rowSpan: 2,
68
+ colSpan: 3,
69
+ flat: true,
70
+ widgetTypeid: 'test-widget',
71
+ widgetState: { color: 'red' },
72
+ });
73
+ });
74
+
75
+ it('should export dashboard with multiple widgets', () => {
76
+ const cell1: CellData = {
77
+ widgetId: WidgetIdUtils.generate(),
78
+ cellId: CellIdUtils.create(1, 1),
79
+ row: 1,
80
+ col: 1,
81
+ rowSpan: 1,
82
+ colSpan: 1,
83
+ widgetFactory: mockWidgetFactory,
84
+ widgetState: {},
85
+ };
86
+
87
+ const cell2: CellData = {
88
+ widgetId: WidgetIdUtils.generate(),
89
+ cellId: CellIdUtils.create(5, 8),
90
+ row: 5,
91
+ col: 8,
92
+ rowSpan: 2,
93
+ colSpan: 4,
94
+ widgetFactory: mockWidgetFactory,
95
+ widgetState: { size: 'large' },
96
+ flat: false,
97
+ };
98
+
99
+ store.addWidget(cell1);
100
+ store.addWidget(cell2);
101
+ const exported = store.exportDashboard();
102
+
103
+ expect(exported.cells.length).toBe(2);
104
+ expect(exported.cells).toContain(jasmine.objectContaining({
105
+ row: 1,
106
+ col: 1,
107
+ rowSpan: 1,
108
+ colSpan: 1,
109
+ flat: undefined,
110
+ widgetTypeid: 'test-widget',
111
+ widgetState: {},
112
+ }));
113
+ expect(exported.cells).toContain(jasmine.objectContaining({
114
+ row: 5,
115
+ col: 8,
116
+ rowSpan: 2,
117
+ colSpan: 4,
118
+ flat: false,
119
+ widgetTypeid: 'test-widget',
120
+ widgetState: { size: 'large' },
121
+ }));
122
+ });
123
+
124
+ it('should export custom grid configuration', () => {
125
+ store.setGridConfig({ rows: 20, columns: 30, gutterSize: '2rem' });
126
+ const exported = store.exportDashboard();
127
+
128
+ expect(exported.rows).toBe(20);
129
+ expect(exported.columns).toBe(30);
130
+ expect(exported.gutterSize).toBe('2rem');
131
+ });
132
+ });
133
+
134
+ describe('loadDashboard', () => {
135
+ it('should load empty dashboard', () => {
136
+ const data: DashboardDataDto = {
137
+ version: '1.0.0',
138
+ dashboardId: 'test-dashboard-1',
139
+ rows: 10,
140
+ columns: 15,
141
+ gutterSize: '1em',
142
+ cells: []
143
+ };
144
+
145
+ store.loadDashboard(data);
146
+
147
+ expect(store.rows()).toBe(10);
148
+ expect(store.columns()).toBe(15);
149
+ expect(store.gutterSize()).toBe('1em');
150
+ expect(store.cells()).toEqual([]);
151
+ });
152
+
153
+ it('should load dashboard with single widget', () => {
154
+ const data: DashboardDataDto = {
155
+ version: '1.0.0',
156
+ dashboardId: 'test-dashboard-2',
157
+ rows: 8,
158
+ columns: 12,
159
+ gutterSize: '0.5em',
160
+ cells: [{
161
+ row: 5,
162
+ col: 7,
163
+ rowSpan: 3,
164
+ colSpan: 2,
165
+ flat: true,
166
+ widgetTypeid: 'test-widget',
167
+ widgetState: { text: 'Hello World' },
168
+ }]
169
+ };
170
+
171
+ store.loadDashboard(data);
172
+
173
+ expect(store.cells().length).toBe(1);
174
+ const loadedCell = store.cells()[0];
175
+ expect(loadedCell.row).toBe(5);
176
+ expect(loadedCell.col).toBe(7);
177
+ expect(loadedCell.rowSpan).toBe(3);
178
+ expect(loadedCell.colSpan).toBe(2);
179
+ expect(loadedCell.flat).toBe(true);
180
+ expect(loadedCell.widgetFactory).toBe(mockWidgetFactory);
181
+ expect(loadedCell.widgetState).toEqual({ text: 'Hello World' });
182
+ expect(loadedCell.cellId).toEqual(CellIdUtils.create(5, 7));
183
+ });
184
+
185
+ it('should load dashboard with multiple widgets', () => {
186
+ const data: DashboardDataDto = {
187
+ version: '1.0.0',
188
+ dashboardId: 'test-dashboard-3',
189
+ rows: 16,
190
+ columns: 16,
191
+ gutterSize: '0.25em',
192
+ cells: [
193
+ {
194
+ row: 2,
195
+ col: 3,
196
+ rowSpan: 1,
197
+ colSpan: 1,
198
+ widgetTypeid: 'test-widget',
199
+ widgetState: {},
200
+ },
201
+ {
202
+ row: 8,
203
+ col: 10,
204
+ rowSpan: 4,
205
+ colSpan: 5,
206
+ flat: false,
207
+ widgetTypeid: 'test-widget',
208
+ widgetState: { config: 'advanced' },
209
+ }
210
+ ]
211
+ };
212
+
213
+ store.loadDashboard(data);
214
+
215
+ expect(store.rows()).toBe(16);
216
+ expect(store.columns()).toBe(16);
217
+ expect(store.gutterSize()).toBe('0.25em');
218
+ expect(store.cells().length).toBe(2);
219
+ });
220
+
221
+ it('should create fallback widgets for unknown widget types', () => {
222
+ const consoleSpy = spyOn(console, 'warn');
223
+ const fallbackFactory = {
224
+ widgetTypeid: '__internal/unknown-widget',
225
+ createInstance: jasmine.createSpy()
226
+ } as any;
227
+
228
+ // Call the real getFactory method which handles fallback logic
229
+ mockDashboardService.getFactory.and.callFake((widgetTypeid: string) => {
230
+ if (widgetTypeid === 'unknown-widget' || widgetTypeid === 'another-unknown-widget') {
231
+ console.warn(`Unknown widget type: ${widgetTypeid}, using fallback error widget`);
232
+ return fallbackFactory;
233
+ }
234
+ return mockWidgetFactory;
235
+ });
236
+
237
+ const data: DashboardDataDto = {
238
+ version: '1.0.0',
239
+ dashboardId: 'test-dashboard-4',
240
+ rows: 8,
241
+ columns: 12,
242
+ gutterSize: '0.5em',
243
+ cells: [
244
+ {
245
+ row: 1,
246
+ col: 1,
247
+ rowSpan: 1,
248
+ colSpan: 1,
249
+ widgetTypeid: 'unknown-widget',
250
+ widgetState: {},
251
+ },
252
+ {
253
+ row: 5,
254
+ col: 5,
255
+ rowSpan: 2,
256
+ colSpan: 2,
257
+ widgetTypeid: 'another-unknown-widget',
258
+ widgetState: {},
259
+ }
260
+ ]
261
+ };
262
+
263
+ store.loadDashboard(data);
264
+
265
+ expect(store.cells().length).toBe(2); // Fallback widgets created instead of skipped
266
+ expect(consoleSpy).toHaveBeenCalledWith('Unknown widget type: unknown-widget, using fallback error widget');
267
+ expect(consoleSpy).toHaveBeenCalledWith('Unknown widget type: another-unknown-widget, using fallback error widget');
268
+ });
269
+
270
+ it('should load mixed valid and invalid widgets', () => {
271
+ const consoleSpy = spyOn(console, 'warn');
272
+ const fallbackFactory = {
273
+ widgetTypeid: '__internal/unknown-widget',
274
+ createInstance: jasmine.createSpy()
275
+ } as any;
276
+
277
+ // Mock factory to return fallback for unknown widgets with console warning
278
+ mockDashboardService.getFactory.and.callFake((widgetTypeid: string) => {
279
+ if (widgetTypeid === 'unknown-widget') {
280
+ console.warn(`Unknown widget type: ${widgetTypeid}, using fallback error widget`);
281
+ return fallbackFactory;
282
+ }
283
+ return mockWidgetFactory;
284
+ });
285
+
286
+ const data: DashboardDataDto = {
287
+ version: '1.0.0',
288
+ dashboardId: 'test-dashboard-5',
289
+ rows: 8,
290
+ columns: 12,
291
+ gutterSize: '0.5em',
292
+ cells: [
293
+ {
294
+ row: 1,
295
+ col: 1,
296
+ rowSpan: 1,
297
+ colSpan: 1,
298
+ widgetTypeid: 'test-widget',
299
+ widgetState: {},
300
+ },
301
+ {
302
+ row: 3,
303
+ col: 3,
304
+ rowSpan: 1,
305
+ colSpan: 1,
306
+ widgetTypeid: 'unknown-widget',
307
+ widgetState: {},
308
+ },
309
+ {
310
+ row: 5,
311
+ col: 5,
312
+ rowSpan: 2,
313
+ colSpan: 2,
314
+ widgetTypeid: 'test-widget',
315
+ widgetState: {},
316
+ }
317
+ ]
318
+ };
319
+
320
+ store.loadDashboard(data);
321
+
322
+ expect(store.cells().length).toBe(3); // All widgets loaded (2 valid + 1 fallback)
323
+ expect(consoleSpy).toHaveBeenCalledWith('Unknown widget type: unknown-widget, using fallback error widget');
324
+ });
325
+
326
+ it('should replace existing dashboard content', () => {
327
+ // Add initial content
328
+ const initialCell: CellData = {
329
+ widgetId: WidgetIdUtils.generate(),
330
+ cellId: CellIdUtils.create(1, 1),
331
+ row: 1,
332
+ col: 1,
333
+ rowSpan: 1,
334
+ colSpan: 1,
335
+ widgetFactory: mockWidgetFactory,
336
+ widgetState: {},
337
+ };
338
+ store.addWidget(initialCell);
339
+ expect(store.cells().length).toBe(1);
340
+
341
+ // Load new data
342
+ const data: DashboardDataDto = {
343
+ version: '1.0.0',
344
+ dashboardId: 'test-dashboard-6',
345
+ rows: 20,
346
+ columns: 25,
347
+ gutterSize: '3rem',
348
+ cells: [
349
+ {
350
+ row: 10,
351
+ col: 15,
352
+ rowSpan: 3,
353
+ colSpan: 4,
354
+ widgetTypeid: 'test-widget',
355
+ widgetState: { replaced: true },
356
+ }
357
+ ]
358
+ };
359
+
360
+ store.loadDashboard(data);
361
+
362
+ expect(store.rows()).toBe(20);
363
+ expect(store.columns()).toBe(25);
364
+ expect(store.gutterSize()).toBe('3rem');
365
+ expect(store.cells().length).toBe(1);
366
+ expect(store.cells()[0].row).toBe(10);
367
+ expect(store.cells()[0].col).toBe(15);
368
+ expect(store.cells()[0].widgetState).toEqual({ replaced: true });
369
+ });
370
+ });
371
+
372
+ describe('exportDashboard - UnknownWidget filtering', () => {
373
+ let unknownWidgetFactory: WidgetFactory;
374
+
375
+ beforeEach(() => {
376
+ unknownWidgetFactory = {
377
+ widgetTypeid: '__internal/unknown-widget',
378
+ name: 'Unknown Widget',
379
+ description: 'Fallback widget',
380
+ svgIcon: '<svg></svg>',
381
+ createInstance: jasmine.createSpy()
382
+ } as any;
383
+ });
384
+
385
+ it('should exclude UnknownWidgetComponent from export', () => {
386
+ const validCell: CellData = {
387
+ widgetId: WidgetIdUtils.generate(),
388
+ cellId: CellIdUtils.create(1, 1),
389
+ row: 1,
390
+ col: 1,
391
+ rowSpan: 1,
392
+ colSpan: 1,
393
+ widgetFactory: mockWidgetFactory,
394
+ widgetState: { valid: true },
395
+ };
396
+
397
+ const unknownCell: CellData = {
398
+ widgetId: WidgetIdUtils.generate(),
399
+ cellId: CellIdUtils.create(2, 2),
400
+ row: 2,
401
+ col: 2,
402
+ rowSpan: 1,
403
+ colSpan: 1,
404
+ widgetFactory: unknownWidgetFactory,
405
+ widgetState: { originalWidgetTypeid: 'missing-widget' },
406
+ };
407
+
408
+ store.addWidget(validCell);
409
+ store.addWidget(unknownCell);
410
+
411
+ expect(store.cells().length).toBe(2); // Both widgets added to store
412
+
413
+ const exported = store.exportDashboard();
414
+
415
+ expect(exported.cells.length).toBe(1); // Only valid widget exported
416
+ expect(exported.cells[0]).toEqual({
417
+ row: 1,
418
+ col: 1,
419
+ rowSpan: 1,
420
+ colSpan: 1,
421
+ flat: undefined,
422
+ widgetTypeid: 'test-widget',
423
+ widgetState: { valid: true },
424
+ });
425
+ });
426
+
427
+ it('should export empty dashboard when only unknown widgets present', () => {
428
+ const unknownCell1: CellData = {
429
+ widgetId: WidgetIdUtils.generate(),
430
+ cellId: CellIdUtils.create(1, 1),
431
+ row: 1,
432
+ col: 1,
433
+ rowSpan: 1,
434
+ colSpan: 1,
435
+ widgetFactory: unknownWidgetFactory,
436
+ widgetState: { originalWidgetTypeid: 'widget-a' },
437
+ };
438
+
439
+ const unknownCell2: CellData = {
440
+ widgetId: WidgetIdUtils.generate(),
441
+ cellId: CellIdUtils.create(3, 3),
442
+ row: 3,
443
+ col: 3,
444
+ rowSpan: 2,
445
+ colSpan: 2,
446
+ widgetFactory: unknownWidgetFactory,
447
+ widgetState: { originalWidgetTypeid: 'widget-b' },
448
+ };
449
+
450
+ store.addWidget(unknownCell1);
451
+ store.addWidget(unknownCell2);
452
+
453
+ expect(store.cells().length).toBe(2); // Both unknown widgets added to store
454
+
455
+ const exported = store.exportDashboard();
456
+
457
+ expect(exported.cells.length).toBe(0); // No widgets exported
458
+ expect(exported.cells).toEqual([]);
459
+ });
460
+
461
+ it('should preserve valid widgets while filtering unknown widgets in mixed scenario', () => {
462
+ const validCell1: CellData = {
463
+ widgetId: WidgetIdUtils.generate(),
464
+ cellId: CellIdUtils.create(1, 1),
465
+ row: 1,
466
+ col: 1,
467
+ rowSpan: 1,
468
+ colSpan: 1,
469
+ widgetFactory: mockWidgetFactory,
470
+ widgetState: { type: 'first' },
471
+ };
472
+
473
+ const unknownCell: CellData = {
474
+ widgetId: WidgetIdUtils.generate(),
475
+ cellId: CellIdUtils.create(2, 2),
476
+ row: 2,
477
+ col: 2,
478
+ rowSpan: 1,
479
+ colSpan: 1,
480
+ widgetFactory: unknownWidgetFactory,
481
+ widgetState: { originalWidgetTypeid: 'missing' },
482
+ };
483
+
484
+ const validCell2: CellData = {
485
+ widgetId: WidgetIdUtils.generate(),
486
+ cellId: CellIdUtils.create(3, 3),
487
+ row: 3,
488
+ col: 3,
489
+ rowSpan: 2,
490
+ colSpan: 3,
491
+ widgetFactory: mockWidgetFactory,
492
+ widgetState: { type: 'second' },
493
+ flat: true,
494
+ };
495
+
496
+ store.addWidget(validCell1);
497
+ store.addWidget(unknownCell);
498
+ store.addWidget(validCell2);
499
+
500
+ expect(store.cells().length).toBe(3); // All widgets added to store
501
+
502
+ const exported = store.exportDashboard();
503
+
504
+ expect(exported.cells.length).toBe(2); // Only valid widgets exported
505
+ expect(exported.cells).toContain(jasmine.objectContaining({
506
+ row: 1,
507
+ col: 1,
508
+ rowSpan: 1,
509
+ colSpan: 1,
510
+ flat: undefined,
511
+ widgetTypeid: 'test-widget',
512
+ widgetState: { type: 'first' },
513
+ }));
514
+ expect(exported.cells).toContain(jasmine.objectContaining({
515
+ row: 3,
516
+ col: 3,
517
+ rowSpan: 2,
518
+ colSpan: 3,
519
+ flat: true,
520
+ widgetTypeid: 'test-widget',
521
+ widgetState: { type: 'second' },
522
+ }));
523
+ });
524
+
525
+ it('should filter unknown widgets with live widget states callback', () => {
526
+ const validCell: CellData = {
527
+ widgetId: WidgetIdUtils.generate(),
528
+ cellId: CellIdUtils.create(1, 1),
529
+ row: 1,
530
+ col: 1,
531
+ rowSpan: 1,
532
+ colSpan: 1,
533
+ widgetFactory: mockWidgetFactory,
534
+ widgetState: { stale: 'data' },
535
+ };
536
+
537
+ const unknownCell: CellData = {
538
+ widgetId: WidgetIdUtils.generate(),
539
+ cellId: CellIdUtils.create(2, 2),
540
+ row: 2,
541
+ col: 2,
542
+ rowSpan: 1,
543
+ colSpan: 1,
544
+ widgetFactory: unknownWidgetFactory,
545
+ widgetState: { originalWidgetTypeid: 'missing' },
546
+ };
547
+
548
+ store.addWidget(validCell);
549
+ store.addWidget(unknownCell);
550
+
551
+ // Mock live widget states for both valid and unknown widgets
552
+ const liveStates = new Map<string, unknown>();
553
+ liveStates.set('1-1', { live: 'valid-data' });
554
+ liveStates.set('2-2', { live: 'unknown-data' }); // This should be ignored
555
+
556
+ const exported = store.exportDashboard(() => liveStates);
557
+
558
+ expect(exported.cells.length).toBe(1); // Only valid widget exported
559
+ expect(exported.cells[0]).toEqual({
560
+ row: 1,
561
+ col: 1,
562
+ rowSpan: 1,
563
+ colSpan: 1,
564
+ flat: undefined,
565
+ widgetTypeid: 'test-widget',
566
+ widgetState: { live: 'valid-data' }, // Uses live state for valid widget
567
+ });
568
+ });
569
+
570
+ it('should maintain grid configuration when filtering unknown widgets', () => {
571
+ store.setGridConfig({ rows: 15, columns: 20, gutterSize: '2em' });
572
+
573
+ const unknownCell: CellData = {
574
+ widgetId: WidgetIdUtils.generate(),
575
+ cellId: CellIdUtils.create(5, 5),
576
+ row: 5,
577
+ col: 5,
578
+ rowSpan: 1,
579
+ colSpan: 1,
580
+ widgetFactory: unknownWidgetFactory,
581
+ widgetState: { originalWidgetTypeid: 'gone' },
582
+ };
583
+
584
+ store.addWidget(unknownCell);
585
+
586
+ const exported = store.exportDashboard();
587
+
588
+ expect(exported.rows).toBe(15);
589
+ expect(exported.columns).toBe(20);
590
+ expect(exported.gutterSize).toBe('2em');
591
+ expect(exported.cells.length).toBe(0); // Unknown widget filtered out
592
+ });
593
+ });
594
+
595
+ describe('exportDashboard with live widget states', () => {
596
+ let mockWidget: jasmine.SpyObj<Widget>;
597
+
598
+ beforeEach(() => {
599
+ mockWidget = jasmine.createSpyObj('Widget', ['dashboardGetState', 'dashboardSetState']);
600
+ });
601
+
602
+ it('should export with live widget states when callback provided', () => {
603
+ const cell: CellData = {
604
+ widgetId: WidgetIdUtils.generate(),
605
+ cellId: CellIdUtils.create(2, 3),
606
+ row: 2,
607
+ col: 3,
608
+ rowSpan: 1,
609
+ colSpan: 1,
610
+ widgetFactory: mockWidgetFactory,
611
+ widgetState: { stale: 'data' }, // This should be ignored
612
+ flat: false,
613
+ };
614
+
615
+ store.addWidget(cell);
616
+
617
+ // Mock live widget states
618
+ const liveStates = new Map<string, unknown>();
619
+ liveStates.set('2-3', { fresh: 'data', updated: true });
620
+
621
+ const exported = store.exportDashboard(() => liveStates);
622
+
623
+ expect(exported.cells.length).toBe(1);
624
+ expect(exported.cells[0]).toEqual({
625
+ row: 2,
626
+ col: 3,
627
+ rowSpan: 1,
628
+ colSpan: 1,
629
+ flat: false,
630
+ widgetTypeid: 'test-widget',
631
+ widgetState: { fresh: 'data', updated: true }, // Uses live state
632
+ });
633
+ });
634
+
635
+ it('should fall back to stored state when live state not available', () => {
636
+ const cell: CellData = {
637
+ widgetId: WidgetIdUtils.generate(),
638
+ cellId: CellIdUtils.create(4, 5),
639
+ row: 4,
640
+ col: 5,
641
+ rowSpan: 2,
642
+ colSpan: 2,
643
+ widgetFactory: mockWidgetFactory,
644
+ widgetState: { fallback: 'state' },
645
+ flat: true,
646
+ };
647
+
648
+ store.addWidget(cell);
649
+
650
+ // Mock live widget states that don't include this cell
651
+ const liveStates = new Map<string, unknown>();
652
+ liveStates.set('1-1', { other: 'widget' });
653
+
654
+ const exported = store.exportDashboard(() => liveStates);
655
+
656
+ expect(exported.cells.length).toBe(1);
657
+ expect(exported.cells[0]).toEqual({
658
+ row: 4,
659
+ col: 5,
660
+ rowSpan: 2,
661
+ colSpan: 2,
662
+ flat: true,
663
+ widgetTypeid: 'test-widget',
664
+ widgetState: { fallback: 'state' }, // Uses stored state
665
+ });
666
+ });
667
+
668
+ it('should handle mixed live and stored states', () => {
669
+ const cell1: CellData = {
670
+ widgetId: WidgetIdUtils.generate(),
671
+ cellId: CellIdUtils.create(1, 1),
672
+ row: 1,
673
+ col: 1,
674
+ rowSpan: 1,
675
+ colSpan: 1,
676
+ widgetFactory: mockWidgetFactory,
677
+ widgetState: { stored: 'state1' },
678
+ };
679
+
680
+ const cell2: CellData = {
681
+ widgetId: WidgetIdUtils.generate(),
682
+ cellId: CellIdUtils.create(2, 2),
683
+ row: 2,
684
+ col: 2,
685
+ rowSpan: 1,
686
+ colSpan: 1,
687
+ widgetFactory: mockWidgetFactory,
688
+ widgetState: { stored: 'state2' },
689
+ };
690
+
691
+ store.addWidget(cell1);
692
+ store.addWidget(cell2);
693
+
694
+ // Only provide live state for one cell
695
+ const liveStates = new Map<string, unknown>();
696
+ liveStates.set('1-1', { live: 'state1', modified: true });
697
+
698
+ const exported = store.exportDashboard(() => liveStates);
699
+
700
+ expect(exported.cells.length).toBe(2);
701
+
702
+ const cell1Export = exported.cells.find(c => c.row === 1 && c.col === 1);
703
+ const cell2Export = exported.cells.find(c => c.row === 2 && c.col === 2);
704
+
705
+ expect(cell1Export?.widgetState).toEqual({ live: 'state1', modified: true });
706
+ expect(cell2Export?.widgetState).toEqual({ stored: 'state2' });
707
+ });
708
+
709
+ it('should work without callback (backward compatibility)', () => {
710
+ const cell: CellData = {
711
+ widgetId: WidgetIdUtils.generate(),
712
+ cellId: CellIdUtils.create(3, 4),
713
+ row: 3,
714
+ col: 4,
715
+ rowSpan: 1,
716
+ colSpan: 1,
717
+ widgetFactory: mockWidgetFactory,
718
+ widgetState: { original: 'state' },
719
+ };
720
+
721
+ store.addWidget(cell);
722
+
723
+ const exported = store.exportDashboard();
724
+
725
+ expect(exported.cells.length).toBe(1);
726
+ expect(exported.cells[0]).toEqual({
727
+ row: 3,
728
+ col: 4,
729
+ rowSpan: 1,
730
+ colSpan: 1,
731
+ flat: undefined,
732
+ widgetTypeid: 'test-widget',
733
+ widgetState: { original: 'state' },
734
+ });
735
+ });
736
+
737
+ it('should handle empty live states map', () => {
738
+ const cell: CellData = {
739
+ widgetId: WidgetIdUtils.generate(),
740
+ cellId: CellIdUtils.create(1, 2),
741
+ row: 1,
742
+ col: 2,
743
+ rowSpan: 1,
744
+ colSpan: 1,
745
+ widgetFactory: mockWidgetFactory,
746
+ widgetState: { default: 'state' },
747
+ };
748
+
749
+ store.addWidget(cell);
750
+
751
+ const exported = store.exportDashboard(() => new Map());
752
+
753
+ expect(exported.cells.length).toBe(1);
754
+ expect(exported.cells[0].widgetState).toEqual({ default: 'state' });
755
+ });
756
+
757
+ it('should handle undefined widget states', () => {
758
+ const cell: CellData = {
759
+ widgetId: WidgetIdUtils.generate(),
760
+ cellId: CellIdUtils.create(5, 6),
761
+ row: 5,
762
+ col: 6,
763
+ rowSpan: 1,
764
+ colSpan: 1,
765
+ widgetFactory: mockWidgetFactory,
766
+ widgetState: { existing: 'state' },
767
+ };
768
+
769
+ store.addWidget(cell);
770
+
771
+ const liveStates = new Map<string, unknown>();
772
+ liveStates.set('5-6', undefined);
773
+
774
+ const exported = store.exportDashboard(() => liveStates);
775
+
776
+ expect(exported.cells.length).toBe(1);
777
+ expect(exported.cells[0].widgetState).toEqual({ existing: 'state' });
778
+ });
779
+ });
780
+ });