@object-ui/plugin-dashboard 3.3.0 → 3.3.2
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/CHANGELOG.md +19 -0
- package/README.md +21 -1
- package/dist/index.js +876 -797
- package/dist/index.umd.cjs +4 -4
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts +5 -0
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/MetricCard.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +4 -1
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +2 -0
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/index.d.ts +1 -1
- package/package.json +40 -7
- package/.turbo/turbo-build.log +0 -41
- package/src/DashboardConfigPanel.stories.tsx +0 -164
- package/src/DashboardConfigPanel.tsx +0 -158
- package/src/DashboardGridLayout.tsx +0 -367
- package/src/DashboardRenderer.stories.tsx +0 -173
- package/src/DashboardRenderer.tsx +0 -479
- package/src/DashboardWithConfig.tsx +0 -211
- package/src/MetricCard.tsx +0 -102
- package/src/MetricWidget.tsx +0 -96
- package/src/ObjectDataTable.tsx +0 -226
- package/src/ObjectMetricWidget.tsx +0 -159
- package/src/ObjectPivotTable.tsx +0 -160
- package/src/PivotTable.tsx +0 -262
- package/src/WidgetConfigPanel.tsx +0 -540
- package/src/__tests__/DashboardConfigPanel.test.tsx +0 -206
- package/src/__tests__/DashboardGridLayout.test.tsx +0 -199
- package/src/__tests__/DashboardRenderer.autoRefresh.test.tsx +0 -124
- package/src/__tests__/DashboardRenderer.designMode.test.tsx +0 -386
- package/src/__tests__/DashboardRenderer.header.test.tsx +0 -114
- package/src/__tests__/DashboardRenderer.mobile.test.tsx +0 -214
- package/src/__tests__/DashboardRenderer.widgetData.test.tsx +0 -1411
- package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
- package/src/__tests__/MetricCard.test.tsx +0 -107
- package/src/__tests__/ObjectDataTable.test.tsx +0 -211
- package/src/__tests__/ObjectMetricWidget.test.tsx +0 -196
- package/src/__tests__/ObjectPivotTable.test.tsx +0 -192
- package/src/__tests__/PivotTable.test.tsx +0 -162
- package/src/__tests__/WidgetConfigPanel.test.tsx +0 -492
- package/src/__tests__/ensureWidgetIds.test.tsx +0 -103
- package/src/index.tsx +0 -236
- package/src/utils.ts +0 -17
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -64
- package/vitest.config.ts +0 -9
- package/vitest.setup.tsx +0 -18
|
@@ -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
|
-
});
|