@object-ui/plugin-dashboard 3.1.5 → 3.3.1

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 (84) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1162 -939
  5. package/dist/index.umd.cjs +4 -4
  6. package/dist/packages/plugin-dashboard/src/DashboardConfigPanel.d.ts.map +1 -0
  7. package/dist/packages/plugin-dashboard/src/DashboardConfigPanel.stories.d.ts.map +1 -0
  8. package/dist/packages/plugin-dashboard/src/DashboardGridLayout.d.ts.map +1 -0
  9. package/dist/{src → packages/plugin-dashboard/src}/DashboardRenderer.d.ts +5 -0
  10. package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts.map +1 -0
  11. package/dist/packages/plugin-dashboard/src/DashboardRenderer.stories.d.ts.map +1 -0
  12. package/dist/packages/plugin-dashboard/src/DashboardWithConfig.d.ts.map +1 -0
  13. package/dist/{src → packages/plugin-dashboard/src}/MetricCard.d.ts +4 -0
  14. package/dist/packages/plugin-dashboard/src/MetricCard.d.ts.map +1 -0
  15. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +31 -0
  16. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -0
  17. package/dist/packages/plugin-dashboard/src/ObjectDataTable.d.ts.map +1 -0
  18. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +59 -0
  19. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -0
  20. package/dist/packages/plugin-dashboard/src/ObjectPivotTable.d.ts.map +1 -0
  21. package/dist/packages/plugin-dashboard/src/PivotTable.d.ts.map +1 -0
  22. package/dist/packages/plugin-dashboard/src/WidgetConfigPanel.d.ts.map +1 -0
  23. package/dist/{src → packages/plugin-dashboard/src}/index.d.ts +4 -2
  24. package/dist/packages/plugin-dashboard/src/index.d.ts.map +1 -0
  25. package/dist/packages/plugin-dashboard/src/utils.d.ts.map +1 -0
  26. package/package.json +44 -11
  27. package/.turbo/turbo-build.log +0 -34
  28. package/dist/src/DashboardConfigPanel.d.ts.map +0 -1
  29. package/dist/src/DashboardConfigPanel.stories.d.ts.map +0 -1
  30. package/dist/src/DashboardGridLayout.d.ts.map +0 -1
  31. package/dist/src/DashboardRenderer.d.ts.map +0 -1
  32. package/dist/src/DashboardRenderer.stories.d.ts.map +0 -1
  33. package/dist/src/DashboardWithConfig.d.ts.map +0 -1
  34. package/dist/src/MetricCard.d.ts.map +0 -1
  35. package/dist/src/MetricWidget.d.ts +0 -24
  36. package/dist/src/MetricWidget.d.ts.map +0 -1
  37. package/dist/src/ObjectDataTable.d.ts.map +0 -1
  38. package/dist/src/ObjectPivotTable.d.ts.map +0 -1
  39. package/dist/src/PivotTable.d.ts.map +0 -1
  40. package/dist/src/WidgetConfigPanel.d.ts.map +0 -1
  41. package/dist/src/index.d.ts.map +0 -1
  42. package/dist/src/utils.d.ts.map +0 -1
  43. package/src/DashboardConfigPanel.stories.tsx +0 -164
  44. package/src/DashboardConfigPanel.tsx +0 -158
  45. package/src/DashboardGridLayout.tsx +0 -367
  46. package/src/DashboardRenderer.stories.tsx +0 -173
  47. package/src/DashboardRenderer.tsx +0 -445
  48. package/src/DashboardWithConfig.tsx +0 -211
  49. package/src/MetricCard.tsx +0 -82
  50. package/src/MetricWidget.tsx +0 -76
  51. package/src/ObjectDataTable.tsx +0 -226
  52. package/src/ObjectPivotTable.tsx +0 -160
  53. package/src/PivotTable.tsx +0 -262
  54. package/src/WidgetConfigPanel.tsx +0 -540
  55. package/src/__tests__/DashboardConfigPanel.test.tsx +0 -206
  56. package/src/__tests__/DashboardGridLayout.test.tsx +0 -199
  57. package/src/__tests__/DashboardRenderer.autoRefresh.test.tsx +0 -124
  58. package/src/__tests__/DashboardRenderer.designMode.test.tsx +0 -386
  59. package/src/__tests__/DashboardRenderer.header.test.tsx +0 -114
  60. package/src/__tests__/DashboardRenderer.mobile.test.tsx +0 -214
  61. package/src/__tests__/DashboardRenderer.widgetData.test.tsx +0 -1283
  62. package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
  63. package/src/__tests__/MetricCard.test.tsx +0 -82
  64. package/src/__tests__/ObjectDataTable.test.tsx +0 -211
  65. package/src/__tests__/ObjectPivotTable.test.tsx +0 -192
  66. package/src/__tests__/PivotTable.test.tsx +0 -162
  67. package/src/__tests__/WidgetConfigPanel.test.tsx +0 -492
  68. package/src/__tests__/ensureWidgetIds.test.tsx +0 -103
  69. package/src/index.tsx +0 -214
  70. package/src/utils.ts +0 -17
  71. package/tsconfig.json +0 -19
  72. package/vite.config.ts +0 -63
  73. package/vitest.config.ts +0 -9
  74. package/vitest.setup.tsx +0 -18
  75. /package/dist/{src → packages/plugin-dashboard/src}/DashboardConfigPanel.d.ts +0 -0
  76. /package/dist/{src → packages/plugin-dashboard/src}/DashboardConfigPanel.stories.d.ts +0 -0
  77. /package/dist/{src → packages/plugin-dashboard/src}/DashboardGridLayout.d.ts +0 -0
  78. /package/dist/{src → packages/plugin-dashboard/src}/DashboardRenderer.stories.d.ts +0 -0
  79. /package/dist/{src → packages/plugin-dashboard/src}/DashboardWithConfig.d.ts +0 -0
  80. /package/dist/{src → packages/plugin-dashboard/src}/ObjectDataTable.d.ts +0 -0
  81. /package/dist/{src → packages/plugin-dashboard/src}/ObjectPivotTable.d.ts +0 -0
  82. /package/dist/{src → packages/plugin-dashboard/src}/PivotTable.d.ts +0 -0
  83. /package/dist/{src → packages/plugin-dashboard/src}/WidgetConfigPanel.d.ts +0 -0
  84. /package/dist/{src → packages/plugin-dashboard/src}/utils.d.ts +0 -0
@@ -1,492 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import { WidgetConfigPanel } from '../WidgetConfigPanel';
12
-
13
- const defaultWidgetConfig = {
14
- title: 'Revenue Chart',
15
- description: 'Monthly revenue breakdown',
16
- type: 'bar',
17
- object: 'orders',
18
- categoryField: 'month',
19
- valueField: 'amount',
20
- aggregate: 'sum',
21
- colorVariant: 'blue',
22
- actionUrl: '',
23
- layoutW: 2,
24
- layoutH: 1,
25
- };
26
-
27
- describe('WidgetConfigPanel', () => {
28
- it('should render nothing when closed', () => {
29
- const { container } = render(
30
- <WidgetConfigPanel
31
- open={false}
32
- onClose={vi.fn()}
33
- config={defaultWidgetConfig}
34
- onSave={vi.fn()}
35
- />,
36
- );
37
- expect(container.innerHTML).toBe('');
38
- });
39
-
40
- it('should render panel when open', () => {
41
- render(
42
- <WidgetConfigPanel
43
- open={true}
44
- onClose={vi.fn()}
45
- config={defaultWidgetConfig}
46
- onSave={vi.fn()}
47
- />,
48
- );
49
- expect(screen.getByTestId('config-panel')).toBeDefined();
50
- });
51
-
52
- it('should display widget breadcrumb', () => {
53
- render(
54
- <WidgetConfigPanel
55
- open={true}
56
- onClose={vi.fn()}
57
- config={defaultWidgetConfig}
58
- onSave={vi.fn()}
59
- />,
60
- );
61
- expect(screen.getByText('Dashboard')).toBeDefined();
62
- // Breadcrumb adapts to widget type — 'bar' type shows "Chart"
63
- expect(screen.getByText('Chart')).toBeDefined();
64
- });
65
-
66
- it('should display general section with title, description, and type fields', () => {
67
- render(
68
- <WidgetConfigPanel
69
- open={true}
70
- onClose={vi.fn()}
71
- config={defaultWidgetConfig}
72
- onSave={vi.fn()}
73
- />,
74
- );
75
- expect(screen.getByText('General')).toBeDefined();
76
- expect(screen.getByText('Title')).toBeDefined();
77
- expect(screen.getByText('Description')).toBeDefined();
78
- expect(screen.getByText('Widget type')).toBeDefined();
79
- });
80
-
81
- it('should display data binding section', () => {
82
- render(
83
- <WidgetConfigPanel
84
- open={true}
85
- onClose={vi.fn()}
86
- config={defaultWidgetConfig}
87
- onSave={vi.fn()}
88
- />,
89
- );
90
- expect(screen.getByText('Data Binding')).toBeDefined();
91
- expect(screen.getByText('Data source')).toBeDefined();
92
- expect(screen.getByText('Category field')).toBeDefined();
93
- expect(screen.getByText('Value field')).toBeDefined();
94
- expect(screen.getByText('Aggregation')).toBeDefined();
95
- });
96
-
97
- it('should display layout section', () => {
98
- render(
99
- <WidgetConfigPanel
100
- open={true}
101
- onClose={vi.fn()}
102
- config={defaultWidgetConfig}
103
- onSave={vi.fn()}
104
- />,
105
- );
106
- expect(screen.getByText('Layout')).toBeDefined();
107
- expect(screen.getByText('Width (columns)')).toBeDefined();
108
- expect(screen.getByText('Height (rows)')).toBeDefined();
109
- });
110
-
111
- it('should have appearance section collapsed by default', () => {
112
- render(
113
- <WidgetConfigPanel
114
- open={true}
115
- onClose={vi.fn()}
116
- config={defaultWidgetConfig}
117
- onSave={vi.fn()}
118
- />,
119
- );
120
- expect(screen.getByText('Appearance')).toBeDefined();
121
- // Fields inside collapsed section should not be visible
122
- expect(screen.queryByText('Color variant')).toBeNull();
123
- expect(screen.queryByText('Action URL')).toBeNull();
124
- });
125
-
126
- it('should expand appearance section on click', () => {
127
- render(
128
- <WidgetConfigPanel
129
- open={true}
130
- onClose={vi.fn()}
131
- config={defaultWidgetConfig}
132
- onSave={vi.fn()}
133
- />,
134
- );
135
- fireEvent.click(screen.getByTestId('section-header-appearance'));
136
- expect(screen.getByText('Color variant')).toBeDefined();
137
- expect(screen.getByText('Action URL')).toBeDefined();
138
- });
139
-
140
- it('should call onClose when close button clicked', () => {
141
- const onClose = vi.fn();
142
- render(
143
- <WidgetConfigPanel
144
- open={true}
145
- onClose={onClose}
146
- config={defaultWidgetConfig}
147
- onSave={vi.fn()}
148
- />,
149
- );
150
- fireEvent.click(screen.getByTestId('config-panel-close'));
151
- expect(onClose).toHaveBeenCalledTimes(1);
152
- });
153
-
154
- it('should show save/discard footer after editing a field', () => {
155
- render(
156
- <WidgetConfigPanel
157
- open={true}
158
- onClose={vi.fn()}
159
- config={defaultWidgetConfig}
160
- onSave={vi.fn()}
161
- />,
162
- );
163
- const titleInput = screen.getByTestId('config-field-title');
164
- fireEvent.change(titleInput, { target: { value: 'New Title' } });
165
- expect(screen.getByTestId('config-panel-footer')).toBeDefined();
166
- });
167
-
168
- it('should call onSave with updated draft', () => {
169
- const onSave = vi.fn();
170
- render(
171
- <WidgetConfigPanel
172
- open={true}
173
- onClose={vi.fn()}
174
- config={defaultWidgetConfig}
175
- onSave={onSave}
176
- />,
177
- );
178
- fireEvent.change(screen.getByTestId('config-field-title'), {
179
- target: { value: 'Updated Chart' },
180
- });
181
- fireEvent.click(screen.getByTestId('config-panel-save'));
182
- expect(onSave).toHaveBeenCalledTimes(1);
183
- const savedDraft = onSave.mock.calls[0][0];
184
- expect(savedDraft.title).toBe('Updated Chart');
185
- });
186
-
187
- it('should revert changes on discard', () => {
188
- render(
189
- <WidgetConfigPanel
190
- open={true}
191
- onClose={vi.fn()}
192
- config={defaultWidgetConfig}
193
- onSave={vi.fn()}
194
- />,
195
- );
196
- const input = screen.getByTestId('config-field-title') as HTMLInputElement;
197
- fireEvent.change(input, { target: { value: 'Temp Title' } });
198
- expect(input.value).toBe('Temp Title');
199
-
200
- fireEvent.click(screen.getByTestId('config-panel-discard'));
201
- expect(screen.queryByTestId('config-panel-footer')).toBeNull();
202
- });
203
-
204
- it('should call onFieldChange for live updates', () => {
205
- const onFieldChange = vi.fn();
206
- render(
207
- <WidgetConfigPanel
208
- open={true}
209
- onClose={vi.fn()}
210
- config={defaultWidgetConfig}
211
- onSave={vi.fn()}
212
- onFieldChange={onFieldChange}
213
- />,
214
- );
215
- fireEvent.change(screen.getByTestId('config-field-title'), {
216
- target: { value: 'Live Update' },
217
- });
218
- expect(onFieldChange).toHaveBeenCalledWith('title', 'Live Update');
219
- });
220
-
221
- it('should populate fields with provided config values', () => {
222
- render(
223
- <WidgetConfigPanel
224
- open={true}
225
- onClose={vi.fn()}
226
- config={defaultWidgetConfig}
227
- onSave={vi.fn()}
228
- />,
229
- );
230
- const titleInput = screen.getByTestId('config-field-title') as HTMLInputElement;
231
- expect(titleInput.value).toBe('Revenue Chart');
232
- const descInput = screen.getByTestId('config-field-description') as HTMLInputElement;
233
- expect(descInput.value).toBe('Monthly revenue breakdown');
234
- });
235
-
236
- // ---- Searchable combobox tests (availableObjects / availableFields) ------
237
-
238
- describe('with availableObjects', () => {
239
- const objects = [
240
- { value: 'accounts', label: 'Accounts' },
241
- { value: 'contacts', label: 'Contacts' },
242
- { value: 'orders', label: 'Orders' },
243
- ];
244
-
245
- it('should render data source as a combobox when availableObjects provided', () => {
246
- render(
247
- <WidgetConfigPanel
248
- open={true}
249
- onClose={vi.fn()}
250
- config={defaultWidgetConfig}
251
- onSave={vi.fn()}
252
- availableObjects={objects}
253
- />,
254
- );
255
- // The data source field should be a combobox (not a text input)
256
- const wrapper = screen.getByTestId('config-field-object');
257
- const comboboxBtn = wrapper.querySelector('[role="combobox"]');
258
- expect(comboboxBtn).toBeTruthy();
259
- });
260
-
261
- it('should render data source as an input when no availableObjects', () => {
262
- render(
263
- <WidgetConfigPanel
264
- open={true}
265
- onClose={vi.fn()}
266
- config={defaultWidgetConfig}
267
- onSave={vi.fn()}
268
- />,
269
- );
270
- const objectField = screen.getByTestId('config-field-object');
271
- expect(objectField.tagName).toBe('INPUT');
272
- });
273
-
274
- it('should render category/value fields as comboboxes when availableObjects provided', () => {
275
- const fields = [
276
- { value: 'name', label: 'Name' },
277
- { value: 'status', label: 'Status' },
278
- ];
279
- render(
280
- <WidgetConfigPanel
281
- open={true}
282
- onClose={vi.fn()}
283
- config={defaultWidgetConfig}
284
- onSave={vi.fn()}
285
- availableObjects={objects}
286
- availableFields={fields}
287
- />,
288
- );
289
- const catWrapper = screen.getByTestId('config-field-categoryField');
290
- expect(catWrapper.querySelector('[role="combobox"]')).toBeTruthy();
291
- const valWrapper = screen.getByTestId('config-field-valueField');
292
- expect(valWrapper.querySelector('[role="combobox"]')).toBeTruthy();
293
- });
294
-
295
- it('should disable field comboboxes when object is not selected', () => {
296
- const configWithNoObject = { ...defaultWidgetConfig, object: '' };
297
- render(
298
- <WidgetConfigPanel
299
- open={true}
300
- onClose={vi.fn()}
301
- config={configWithNoObject}
302
- onSave={vi.fn()}
303
- availableObjects={objects}
304
- />,
305
- );
306
- const catWrapper = screen.getByTestId('config-field-categoryField');
307
- const catBtn = catWrapper.querySelector('[role="combobox"]');
308
- expect(catBtn).toHaveAttribute('disabled');
309
- const valWrapper = screen.getByTestId('config-field-valueField');
310
- const valBtn = valWrapper.querySelector('[role="combobox"]');
311
- expect(valBtn).toHaveAttribute('disabled');
312
- });
313
-
314
- it('should not disable field comboboxes when object is selected', () => {
315
- const fields = [
316
- { value: 'name', label: 'Name' },
317
- { value: 'amount', label: 'Amount' },
318
- ];
319
- render(
320
- <WidgetConfigPanel
321
- open={true}
322
- onClose={vi.fn()}
323
- config={defaultWidgetConfig}
324
- onSave={vi.fn()}
325
- availableObjects={objects}
326
- availableFields={fields}
327
- />,
328
- );
329
- const catWrapper = screen.getByTestId('config-field-categoryField');
330
- const catBtn = catWrapper.querySelector('[role="combobox"]');
331
- expect(catBtn).not.toHaveAttribute('disabled');
332
- const valWrapper = screen.getByTestId('config-field-valueField');
333
- const valBtn = valWrapper.querySelector('[role="combobox"]');
334
- expect(valBtn).not.toHaveAttribute('disabled');
335
- });
336
- });
337
-
338
- // ---- Context-aware sections (visibleWhen) ---------------------------------
339
-
340
- describe('context-aware sections', () => {
341
- it('should show pivot-specific sections when type is pivot', () => {
342
- const pivotConfig = { ...defaultWidgetConfig, type: 'pivot' };
343
- render(
344
- <WidgetConfigPanel
345
- open={true}
346
- onClose={vi.fn()}
347
- config={pivotConfig}
348
- onSave={vi.fn()}
349
- />,
350
- );
351
- // Pivot sections should be visible
352
- expect(screen.getByText('Rows')).toBeDefined();
353
- expect(screen.getByText('Columns')).toBeDefined();
354
- expect(screen.getByText('Values')).toBeDefined();
355
- // Data Binding (chart/metric section) should NOT be visible
356
- expect(screen.queryByText('Data Binding')).toBeNull();
357
- });
358
-
359
- it('should show chart-specific sections when type is bar', () => {
360
- render(
361
- <WidgetConfigPanel
362
- open={true}
363
- onClose={vi.fn()}
364
- config={defaultWidgetConfig}
365
- onSave={vi.fn()}
366
- />,
367
- );
368
- // Chart Axis & Series section should be visible
369
- expect(screen.getByText('Axis & Series')).toBeDefined();
370
- // Pivot sections should NOT be visible
371
- expect(screen.queryByText('Rows')).toBeNull();
372
- expect(screen.queryByText('Values')).toBeNull();
373
- });
374
-
375
- it('should show table-specific sections when type is table', () => {
376
- const tableConfig = { ...defaultWidgetConfig, type: 'table' };
377
- render(
378
- <WidgetConfigPanel
379
- open={true}
380
- onClose={vi.fn()}
381
- config={tableConfig}
382
- onSave={vi.fn()}
383
- />,
384
- );
385
- // Table Columns section should be visible
386
- expect(screen.getByText('Columns')).toBeDefined();
387
- // Chart Axis & Series should NOT be visible
388
- expect(screen.queryByText('Axis & Series')).toBeNull();
389
- // Pivot sections should NOT be visible
390
- expect(screen.queryByText('Rows')).toBeNull();
391
- });
392
-
393
- it('should hide chart/pivot/table sections for metric type', () => {
394
- const metricConfig = { ...defaultWidgetConfig, type: 'metric' };
395
- render(
396
- <WidgetConfigPanel
397
- open={true}
398
- onClose={vi.fn()}
399
- config={metricConfig}
400
- onSave={vi.fn()}
401
- />,
402
- );
403
- expect(screen.queryByText('Axis & Series')).toBeNull();
404
- expect(screen.queryByText('Rows')).toBeNull();
405
- expect(screen.queryByText('Values')).toBeNull();
406
- // Data Binding should still be visible
407
- expect(screen.getByText('Data Binding')).toBeDefined();
408
- });
409
-
410
- it('should show breadcrumb "Pivot table" for pivot type', () => {
411
- const pivotConfig = { ...defaultWidgetConfig, type: 'pivot' };
412
- render(
413
- <WidgetConfigPanel
414
- open={true}
415
- onClose={vi.fn()}
416
- config={pivotConfig}
417
- onSave={vi.fn()}
418
- />,
419
- );
420
- expect(screen.getByText('Pivot table')).toBeDefined();
421
- });
422
-
423
- it('should show breadcrumb "Table" for table type', () => {
424
- const tableConfig = { ...defaultWidgetConfig, type: 'table' };
425
- render(
426
- <WidgetConfigPanel
427
- open={true}
428
- onClose={vi.fn()}
429
- config={tableConfig}
430
- onSave={vi.fn()}
431
- />,
432
- );
433
- // "Table" appears in both breadcrumb and the select option
434
- const tableElements = screen.getAllByText('Table');
435
- expect(tableElements.length).toBeGreaterThanOrEqual(2);
436
- });
437
- });
438
-
439
- // ---- I18nLabel resolution -------------------------------------------------
440
-
441
- describe('I18nLabel resolution', () => {
442
- it('should resolve I18nLabel object for title field', () => {
443
- const configWithI18n = {
444
- ...defaultWidgetConfig,
445
- title: { key: 'widget.title', defaultValue: 'Revenue Chart (i18n)' },
446
- };
447
- render(
448
- <WidgetConfigPanel
449
- open={true}
450
- onClose={vi.fn()}
451
- config={configWithI18n}
452
- onSave={vi.fn()}
453
- />,
454
- );
455
- const titleInput = screen.getByTestId('config-field-title') as HTMLInputElement;
456
- expect(titleInput.value).toBe('Revenue Chart (i18n)');
457
- });
458
-
459
- it('should resolve I18nLabel object for description field', () => {
460
- const configWithI18n = {
461
- ...defaultWidgetConfig,
462
- description: { key: 'widget.desc', defaultValue: 'Description from i18n' },
463
- };
464
- render(
465
- <WidgetConfigPanel
466
- open={true}
467
- onClose={vi.fn()}
468
- config={configWithI18n}
469
- onSave={vi.fn()}
470
- />,
471
- );
472
- const descInput = screen.getByTestId('config-field-description') as HTMLInputElement;
473
- expect(descInput.value).toBe('Description from i18n');
474
- });
475
- });
476
-
477
- // ---- Pivot Table type in widget options -----------------------------------
478
-
479
- it('should include Pivot Table in widget type options', () => {
480
- const pivotConfig = { ...defaultWidgetConfig, type: 'pivot' };
481
- render(
482
- <WidgetConfigPanel
483
- open={true}
484
- onClose={vi.fn()}
485
- config={pivotConfig}
486
- onSave={vi.fn()}
487
- />,
488
- );
489
- // The select should contain pivot option
490
- expect(screen.getByText('Pivot Table')).toBeDefined();
491
- });
492
- });
@@ -1,103 +0,0 @@
1
- /**
2
- * Tests for the ensureWidgetIds helper pattern.
3
- * Validates that widgets without IDs get auto-assigned unique IDs.
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import type { DashboardSchema } from '@object-ui/types';
8
-
9
- /**
10
- * Portable implementation of ensureWidgetIds — mirrors
11
- * apps/console/src/components/DashboardView.tsx.
12
- */
13
- let counter = 0;
14
- function createWidgetId(): string {
15
- counter += 1;
16
- return `widget_test_${counter}`;
17
- }
18
-
19
- function ensureWidgetIds(schema: DashboardSchema): DashboardSchema {
20
- if (!schema.widgets?.length) return schema;
21
- const needsFix = schema.widgets.some((w) => !w.id);
22
- if (!needsFix) return schema;
23
- return {
24
- ...schema,
25
- widgets: schema.widgets.map((w) => (w.id ? w : { ...w, id: createWidgetId() })),
26
- };
27
- }
28
-
29
- describe('ensureWidgetIds', () => {
30
- beforeEach(() => {
31
- counter = 0;
32
- });
33
- it('should return same schema when all widgets have IDs', () => {
34
- const schema: DashboardSchema = {
35
- type: 'dashboard',
36
- columns: 3,
37
- widgets: [
38
- { id: 'w1', title: 'A', type: 'metric' },
39
- { id: 'w2', title: 'B', type: 'bar' },
40
- ],
41
- };
42
- const result = ensureWidgetIds(schema);
43
- expect(result).toBe(schema); // same reference — no mutation
44
- });
45
-
46
- it('should assign IDs to widgets missing them', () => {
47
- const schema: DashboardSchema = {
48
- type: 'dashboard',
49
- columns: 3,
50
- widgets: [
51
- { title: 'No ID', type: 'metric' },
52
- { id: 'w2', title: 'Has ID', type: 'bar' },
53
- ],
54
- };
55
- const result = ensureWidgetIds(schema);
56
- expect(result).not.toBe(schema);
57
- expect(result.widgets[0].id).toBeTruthy();
58
- expect(result.widgets[1].id).toBe('w2');
59
- });
60
-
61
- it('should handle empty widgets array', () => {
62
- const schema: DashboardSchema = {
63
- type: 'dashboard',
64
- columns: 3,
65
- widgets: [],
66
- };
67
- const result = ensureWidgetIds(schema);
68
- expect(result).toBe(schema);
69
- });
70
-
71
- it('should generate unique IDs for multiple missing widgets', () => {
72
- const schema: DashboardSchema = {
73
- type: 'dashboard',
74
- columns: 3,
75
- widgets: [
76
- { title: 'A', type: 'metric' },
77
- { title: 'B', type: 'bar' },
78
- { title: 'C', type: 'pie' },
79
- ],
80
- };
81
- const result = ensureWidgetIds(schema);
82
- const ids = result.widgets.map((w) => w.id);
83
- // All IDs should be unique
84
- expect(new Set(ids).size).toBe(ids.length);
85
- // All IDs should be truthy
86
- ids.forEach((id) => expect(id).toBeTruthy());
87
- });
88
-
89
- it('should preserve existing widget data', () => {
90
- const schema: DashboardSchema = {
91
- type: 'dashboard',
92
- columns: 3,
93
- widgets: [
94
- { title: 'Revenue', type: 'metric', object: 'orders', layout: { x: 0, y: 0, w: 2, h: 1 } },
95
- ],
96
- };
97
- const result = ensureWidgetIds(schema);
98
- expect(result.widgets[0].title).toBe('Revenue');
99
- expect(result.widgets[0].type).toBe('metric');
100
- expect(result.widgets[0].object).toBe('orders');
101
- expect(result.widgets[0].layout).toEqual({ x: 0, y: 0, w: 2, h: 1 });
102
- });
103
- });