@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.
- package/CHANGELOG.md +28 -0
- package/README.md +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1162 -939
- package/dist/index.umd.cjs +4 -4
- package/dist/packages/plugin-dashboard/src/DashboardConfigPanel.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/DashboardConfigPanel.stories.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/DashboardGridLayout.d.ts.map +1 -0
- package/dist/{src → packages/plugin-dashboard/src}/DashboardRenderer.d.ts +5 -0
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.stories.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/DashboardWithConfig.d.ts.map +1 -0
- package/dist/{src → packages/plugin-dashboard/src}/MetricCard.d.ts +4 -0
- package/dist/packages/plugin-dashboard/src/MetricCard.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +31 -0
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/ObjectDataTable.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +59 -0
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/ObjectPivotTable.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/PivotTable.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/WidgetConfigPanel.d.ts.map +1 -0
- package/dist/{src → packages/plugin-dashboard/src}/index.d.ts +4 -2
- package/dist/packages/plugin-dashboard/src/index.d.ts.map +1 -0
- package/dist/packages/plugin-dashboard/src/utils.d.ts.map +1 -0
- package/package.json +44 -11
- package/.turbo/turbo-build.log +0 -34
- package/dist/src/DashboardConfigPanel.d.ts.map +0 -1
- package/dist/src/DashboardConfigPanel.stories.d.ts.map +0 -1
- package/dist/src/DashboardGridLayout.d.ts.map +0 -1
- package/dist/src/DashboardRenderer.d.ts.map +0 -1
- package/dist/src/DashboardRenderer.stories.d.ts.map +0 -1
- package/dist/src/DashboardWithConfig.d.ts.map +0 -1
- package/dist/src/MetricCard.d.ts.map +0 -1
- package/dist/src/MetricWidget.d.ts +0 -24
- package/dist/src/MetricWidget.d.ts.map +0 -1
- package/dist/src/ObjectDataTable.d.ts.map +0 -1
- package/dist/src/ObjectPivotTable.d.ts.map +0 -1
- package/dist/src/PivotTable.d.ts.map +0 -1
- package/dist/src/WidgetConfigPanel.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/utils.d.ts.map +0 -1
- 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 -445
- package/src/DashboardWithConfig.tsx +0 -211
- package/src/MetricCard.tsx +0 -82
- package/src/MetricWidget.tsx +0 -76
- package/src/ObjectDataTable.tsx +0 -226
- 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 -1283
- package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
- package/src/__tests__/MetricCard.test.tsx +0 -82
- package/src/__tests__/ObjectDataTable.test.tsx +0 -211
- 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 -214
- package/src/utils.ts +0 -17
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -63
- package/vitest.config.ts +0 -9
- package/vitest.setup.tsx +0 -18
- /package/dist/{src → packages/plugin-dashboard/src}/DashboardConfigPanel.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/DashboardConfigPanel.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/DashboardGridLayout.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/DashboardRenderer.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/DashboardWithConfig.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/ObjectDataTable.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/ObjectPivotTable.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/PivotTable.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/WidgetConfigPanel.d.ts +0 -0
- /package/dist/{src → packages/plugin-dashboard/src}/utils.d.ts +0 -0
|
@@ -1,1283 +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 } from 'vitest';
|
|
10
|
-
import { render } from '@testing-library/react';
|
|
11
|
-
import { DashboardRenderer } from '../DashboardRenderer';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Extract component schemas rendered by SchemaRenderer from the DOM.
|
|
15
|
-
* When a component type is not registered, SchemaRenderer renders
|
|
16
|
-
* an error block containing a JSON <pre> element with the schema.
|
|
17
|
-
* We parse those to verify the schema shape produced by DashboardRenderer.
|
|
18
|
-
*/
|
|
19
|
-
function getRenderedSchemas(container: HTMLElement): any[] {
|
|
20
|
-
const pres = container.querySelectorAll('pre');
|
|
21
|
-
return Array.from(pres).map(el => JSON.parse(el.textContent!));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('DashboardRenderer widget data extraction', () => {
|
|
25
|
-
it('should extract chart data from options.data.items', () => {
|
|
26
|
-
const schema = {
|
|
27
|
-
type: 'dashboard' as const,
|
|
28
|
-
name: 'test',
|
|
29
|
-
title: 'Test',
|
|
30
|
-
widgets: [
|
|
31
|
-
{
|
|
32
|
-
type: 'bar',
|
|
33
|
-
title: 'Test Bar',
|
|
34
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
35
|
-
options: {
|
|
36
|
-
xField: 'name',
|
|
37
|
-
yField: 'value',
|
|
38
|
-
data: {
|
|
39
|
-
provider: 'value',
|
|
40
|
-
items: [
|
|
41
|
-
{ name: 'A', value: 100 },
|
|
42
|
-
{ name: 'B', value: 200 },
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
} as any;
|
|
49
|
-
|
|
50
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
51
|
-
const schemas = getRenderedSchemas(container);
|
|
52
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
53
|
-
|
|
54
|
-
expect(chartSchema).toBeDefined();
|
|
55
|
-
expect(chartSchema.chartType).toBe('bar');
|
|
56
|
-
expect(chartSchema.data).toHaveLength(2);
|
|
57
|
-
expect(chartSchema.data[0]).toEqual({ name: 'A', value: 100 });
|
|
58
|
-
expect(chartSchema.xAxisKey).toBe('name');
|
|
59
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'value' }]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should extract chart data from widget.data.items (backward compat)', () => {
|
|
63
|
-
const schema = {
|
|
64
|
-
type: 'dashboard' as const,
|
|
65
|
-
name: 'test',
|
|
66
|
-
title: 'Test',
|
|
67
|
-
widgets: [
|
|
68
|
-
{
|
|
69
|
-
type: 'area',
|
|
70
|
-
title: 'Test Area',
|
|
71
|
-
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
72
|
-
options: { xField: 'month', yField: 'revenue' },
|
|
73
|
-
data: {
|
|
74
|
-
provider: 'value',
|
|
75
|
-
items: [
|
|
76
|
-
{ month: 'Jan', revenue: 155000 },
|
|
77
|
-
{ month: 'Feb', revenue: 87000 },
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
} as any;
|
|
83
|
-
|
|
84
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
85
|
-
const schemas = getRenderedSchemas(container);
|
|
86
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
87
|
-
|
|
88
|
-
expect(chartSchema).toBeDefined();
|
|
89
|
-
expect(chartSchema.chartType).toBe('area');
|
|
90
|
-
expect(chartSchema.data).toHaveLength(2);
|
|
91
|
-
expect(chartSchema.data[0].month).toBe('Jan');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should extract table data from options.data.items', () => {
|
|
95
|
-
const schema = {
|
|
96
|
-
type: 'dashboard' as const,
|
|
97
|
-
name: 'test',
|
|
98
|
-
title: 'Test',
|
|
99
|
-
widgets: [
|
|
100
|
-
{
|
|
101
|
-
type: 'table',
|
|
102
|
-
title: 'Test Table',
|
|
103
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
104
|
-
options: {
|
|
105
|
-
columns: [
|
|
106
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
107
|
-
{ header: 'Amount', accessorKey: 'amount' },
|
|
108
|
-
],
|
|
109
|
-
data: {
|
|
110
|
-
provider: 'value',
|
|
111
|
-
items: [
|
|
112
|
-
{ name: 'Item A', amount: '$100' },
|
|
113
|
-
{ name: 'Item B', amount: '$200' },
|
|
114
|
-
{ name: 'Item C', amount: '$300' },
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
} as any;
|
|
121
|
-
|
|
122
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
123
|
-
// data-table is a registered component that renders a real table,
|
|
124
|
-
// so we verify the data reaches it by checking for rendered cell content
|
|
125
|
-
expect(container.textContent).toContain('Item A');
|
|
126
|
-
expect(container.textContent).toContain('$200');
|
|
127
|
-
expect(container.textContent).toContain('Item C');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should handle donut chart data from options', () => {
|
|
131
|
-
const schema = {
|
|
132
|
-
type: 'dashboard' as const,
|
|
133
|
-
name: 'test',
|
|
134
|
-
title: 'Test',
|
|
135
|
-
widgets: [
|
|
136
|
-
{
|
|
137
|
-
type: 'donut',
|
|
138
|
-
title: 'Test Donut',
|
|
139
|
-
layout: { x: 0, y: 0, w: 1, h: 2 },
|
|
140
|
-
options: {
|
|
141
|
-
xField: 'source',
|
|
142
|
-
yField: 'value',
|
|
143
|
-
data: {
|
|
144
|
-
provider: 'value',
|
|
145
|
-
items: [
|
|
146
|
-
{ source: 'Web', value: 2 },
|
|
147
|
-
{ source: 'Referral', value: 1 },
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
} as any;
|
|
154
|
-
|
|
155
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
156
|
-
const schemas = getRenderedSchemas(container);
|
|
157
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
158
|
-
|
|
159
|
-
expect(chartSchema).toBeDefined();
|
|
160
|
-
expect(chartSchema.chartType).toBe('donut');
|
|
161
|
-
expect(chartSchema.data).toHaveLength(2);
|
|
162
|
-
expect(chartSchema.xAxisKey).toBe('source');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should default to empty array when no data is provided', () => {
|
|
166
|
-
const schema = {
|
|
167
|
-
type: 'dashboard' as const,
|
|
168
|
-
name: 'test',
|
|
169
|
-
title: 'Test',
|
|
170
|
-
widgets: [
|
|
171
|
-
{
|
|
172
|
-
type: 'bar',
|
|
173
|
-
title: 'No Data Bar',
|
|
174
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
175
|
-
options: { xField: 'x', yField: 'y' },
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
} as any;
|
|
179
|
-
|
|
180
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
181
|
-
const schemas = getRenderedSchemas(container);
|
|
182
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
183
|
-
|
|
184
|
-
expect(chartSchema).toBeDefined();
|
|
185
|
-
expect(chartSchema.data).toEqual([]);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should render metric widgets using spec shorthand format', () => {
|
|
189
|
-
const schema = {
|
|
190
|
-
type: 'dashboard' as const,
|
|
191
|
-
name: 'test',
|
|
192
|
-
title: 'Test',
|
|
193
|
-
widgets: [
|
|
194
|
-
{
|
|
195
|
-
type: 'metric',
|
|
196
|
-
layout: { x: 0, y: 0, w: 1, h: 1 },
|
|
197
|
-
options: {
|
|
198
|
-
label: 'Total Revenue',
|
|
199
|
-
value: '$652,000',
|
|
200
|
-
trend: { value: 12.5, direction: 'up', label: 'vs last month' },
|
|
201
|
-
icon: 'DollarSign',
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
type: 'metric',
|
|
206
|
-
layout: { x: 1, y: 0, w: 1, h: 1 },
|
|
207
|
-
options: {
|
|
208
|
-
label: 'Active Deals',
|
|
209
|
-
value: '5',
|
|
210
|
-
trend: { value: 2.1, direction: 'down', label: 'vs last month' },
|
|
211
|
-
icon: 'Briefcase',
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
} as any;
|
|
216
|
-
|
|
217
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
218
|
-
|
|
219
|
-
// MetricWidget is registered in the ComponentRegistry, so it should render
|
|
220
|
-
// the label and value from the merged options
|
|
221
|
-
expect(container.textContent).toContain('Total Revenue');
|
|
222
|
-
expect(container.textContent).toContain('$652,000');
|
|
223
|
-
expect(container.textContent).toContain('Active Deals');
|
|
224
|
-
expect(container.textContent).toContain('5');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should render metric widgets with I18nLabel objects without crashing', () => {
|
|
228
|
-
const schema = {
|
|
229
|
-
type: 'dashboard' as const,
|
|
230
|
-
name: 'test',
|
|
231
|
-
title: 'Test',
|
|
232
|
-
widgets: [
|
|
233
|
-
{
|
|
234
|
-
type: 'metric',
|
|
235
|
-
layout: { x: 0, y: 0, w: 1, h: 1 },
|
|
236
|
-
options: {
|
|
237
|
-
label: 'Total Revenue',
|
|
238
|
-
value: '$652,000',
|
|
239
|
-
trend: { value: 12.5, direction: 'up', label: { key: 'crm.dashboard.trendLabel', defaultValue: 'vs last month' } },
|
|
240
|
-
icon: 'DollarSign',
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
type: 'metric',
|
|
245
|
-
layout: { x: 1, y: 0, w: 1, h: 1 },
|
|
246
|
-
options: {
|
|
247
|
-
label: 'Active Deals',
|
|
248
|
-
value: '5',
|
|
249
|
-
trend: { value: 2.1, direction: 'down', label: { key: 'crm.dashboard.trendLabel', defaultValue: 'vs last month' } },
|
|
250
|
-
icon: 'Briefcase',
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
} as any;
|
|
255
|
-
|
|
256
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
257
|
-
|
|
258
|
-
// Should resolve I18nLabel objects to their defaultValue strings
|
|
259
|
-
expect(container.textContent).toContain('Total Revenue');
|
|
260
|
-
expect(container.textContent).toContain('$652,000');
|
|
261
|
-
expect(container.textContent).toContain('vs last month');
|
|
262
|
-
expect(container.textContent).toContain('Active Deals');
|
|
263
|
-
expect(container.textContent).toContain('5');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should assign unique keys to widgets without id or title', () => {
|
|
267
|
-
const schema = {
|
|
268
|
-
type: 'dashboard' as const,
|
|
269
|
-
name: 'test',
|
|
270
|
-
title: 'Test',
|
|
271
|
-
widgets: [
|
|
272
|
-
{
|
|
273
|
-
type: 'metric',
|
|
274
|
-
layout: { x: 0, y: 0, w: 1, h: 1 },
|
|
275
|
-
options: { label: 'Metric A', value: '100' },
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
type: 'metric',
|
|
279
|
-
layout: { x: 1, y: 0, w: 1, h: 1 },
|
|
280
|
-
options: { label: 'Metric B', value: '200' },
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
type: 'metric',
|
|
284
|
-
layout: { x: 2, y: 0, w: 1, h: 1 },
|
|
285
|
-
options: { label: 'Metric C', value: '300' },
|
|
286
|
-
},
|
|
287
|
-
],
|
|
288
|
-
} as any;
|
|
289
|
-
|
|
290
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
291
|
-
|
|
292
|
-
// All three metrics should render without React key warnings
|
|
293
|
-
expect(container.textContent).toContain('Metric A');
|
|
294
|
-
expect(container.textContent).toContain('Metric B');
|
|
295
|
-
expect(container.textContent).toContain('Metric C');
|
|
296
|
-
expect(container.textContent).toContain('100');
|
|
297
|
-
expect(container.textContent).toContain('200');
|
|
298
|
-
expect(container.textContent).toContain('300');
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('should produce object-chart schema for chart widgets with provider: object', () => {
|
|
302
|
-
const schema = {
|
|
303
|
-
type: 'dashboard' as const,
|
|
304
|
-
name: 'test',
|
|
305
|
-
title: 'Test',
|
|
306
|
-
widgets: [
|
|
307
|
-
{
|
|
308
|
-
type: 'bar',
|
|
309
|
-
title: 'Revenue by Account',
|
|
310
|
-
object: 'opportunity',
|
|
311
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
312
|
-
options: {
|
|
313
|
-
xField: 'account',
|
|
314
|
-
yField: 'total',
|
|
315
|
-
data: {
|
|
316
|
-
provider: 'object',
|
|
317
|
-
object: 'opportunity',
|
|
318
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'account' },
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
} as any;
|
|
324
|
-
|
|
325
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
326
|
-
const schemas = getRenderedSchemas(container);
|
|
327
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
328
|
-
|
|
329
|
-
expect(chartSchema).toBeDefined();
|
|
330
|
-
expect(chartSchema.chartType).toBe('bar');
|
|
331
|
-
expect(chartSchema.objectName).toBe('opportunity');
|
|
332
|
-
expect(chartSchema.aggregate).toEqual({
|
|
333
|
-
field: 'amount',
|
|
334
|
-
function: 'sum',
|
|
335
|
-
groupBy: 'account',
|
|
336
|
-
});
|
|
337
|
-
expect(chartSchema.xAxisKey).toBe('account');
|
|
338
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'amount' }]);
|
|
339
|
-
// Must NOT have an empty data array – data comes from the object source
|
|
340
|
-
expect(chartSchema.data).toBeUndefined();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it('should fall back to widget.object when data.object is missing for provider: object', () => {
|
|
344
|
-
const schema = {
|
|
345
|
-
type: 'dashboard' as const,
|
|
346
|
-
name: 'test',
|
|
347
|
-
title: 'Test',
|
|
348
|
-
widgets: [
|
|
349
|
-
{
|
|
350
|
-
type: 'area',
|
|
351
|
-
title: 'Trend',
|
|
352
|
-
object: 'deal',
|
|
353
|
-
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
354
|
-
options: {
|
|
355
|
-
xField: 'month',
|
|
356
|
-
yField: 'revenue',
|
|
357
|
-
data: {
|
|
358
|
-
provider: 'object',
|
|
359
|
-
aggregate: { field: 'revenue', function: 'sum', groupBy: 'month' },
|
|
360
|
-
},
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
],
|
|
364
|
-
} as any;
|
|
365
|
-
|
|
366
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
367
|
-
const schemas = getRenderedSchemas(container);
|
|
368
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
369
|
-
|
|
370
|
-
expect(chartSchema).toBeDefined();
|
|
371
|
-
expect(chartSchema.objectName).toBe('deal');
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it('should pass through provider: object config for table widgets', () => {
|
|
375
|
-
const schema = {
|
|
376
|
-
type: 'dashboard' as const,
|
|
377
|
-
name: 'test',
|
|
378
|
-
title: 'Test',
|
|
379
|
-
widgets: [
|
|
380
|
-
{
|
|
381
|
-
type: 'table',
|
|
382
|
-
title: 'Object Table',
|
|
383
|
-
object: 'opportunity',
|
|
384
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
385
|
-
options: {
|
|
386
|
-
columns: [
|
|
387
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
388
|
-
{ header: 'Amount', accessorKey: 'amount' },
|
|
389
|
-
],
|
|
390
|
-
data: {
|
|
391
|
-
provider: 'object',
|
|
392
|
-
object: 'opportunity',
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
} as any;
|
|
398
|
-
|
|
399
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
400
|
-
const schemas = getRenderedSchemas(container);
|
|
401
|
-
// DashboardRenderer now routes object-bound tables to 'object-data-table'
|
|
402
|
-
const tableSchema = schemas.find(s => s.type === 'object-data-table');
|
|
403
|
-
|
|
404
|
-
if (tableSchema) {
|
|
405
|
-
expect(tableSchema.objectName).toBe('opportunity');
|
|
406
|
-
expect(tableSchema.dataProvider).toEqual({
|
|
407
|
-
provider: 'object',
|
|
408
|
-
object: 'opportunity',
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it('should pass through provider: object config for pivot widgets', () => {
|
|
414
|
-
const schema = {
|
|
415
|
-
type: 'dashboard' as const,
|
|
416
|
-
name: 'test',
|
|
417
|
-
title: 'Test',
|
|
418
|
-
widgets: [
|
|
419
|
-
{
|
|
420
|
-
type: 'pivot',
|
|
421
|
-
title: 'Object Pivot',
|
|
422
|
-
object: 'sales',
|
|
423
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
424
|
-
options: {
|
|
425
|
-
rowField: 'region',
|
|
426
|
-
columnField: 'quarter',
|
|
427
|
-
valueField: 'revenue',
|
|
428
|
-
data: {
|
|
429
|
-
provider: 'object',
|
|
430
|
-
object: 'sales',
|
|
431
|
-
},
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
],
|
|
435
|
-
} as any;
|
|
436
|
-
|
|
437
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
438
|
-
const schemas = getRenderedSchemas(container);
|
|
439
|
-
// DashboardRenderer now routes object-bound pivots to 'object-pivot'
|
|
440
|
-
const pivotSchema = schemas.find(s => s.type === 'object-pivot');
|
|
441
|
-
|
|
442
|
-
if (pivotSchema) {
|
|
443
|
-
expect(pivotSchema.objectName).toBe('sales');
|
|
444
|
-
expect(pivotSchema.dataProvider).toEqual({
|
|
445
|
-
provider: 'object',
|
|
446
|
-
object: 'sales',
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it('should use yField as series dataKey when provider: object has no aggregate', () => {
|
|
452
|
-
const schema = {
|
|
453
|
-
type: 'dashboard' as const,
|
|
454
|
-
name: 'test',
|
|
455
|
-
title: 'Test',
|
|
456
|
-
widgets: [
|
|
457
|
-
{
|
|
458
|
-
type: 'line',
|
|
459
|
-
title: 'No Aggregate',
|
|
460
|
-
object: 'opportunity',
|
|
461
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
462
|
-
options: {
|
|
463
|
-
xField: 'date',
|
|
464
|
-
yField: 'revenue',
|
|
465
|
-
data: {
|
|
466
|
-
provider: 'object',
|
|
467
|
-
object: 'opportunity',
|
|
468
|
-
},
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
|
-
],
|
|
472
|
-
} as any;
|
|
473
|
-
|
|
474
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
475
|
-
const schemas = getRenderedSchemas(container);
|
|
476
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
477
|
-
|
|
478
|
-
expect(chartSchema).toBeDefined();
|
|
479
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'revenue' }]);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it('should auto-adapt series dataKey from aggregate.field even when yField differs', () => {
|
|
483
|
-
const schema = {
|
|
484
|
-
type: 'dashboard' as const,
|
|
485
|
-
name: 'test',
|
|
486
|
-
title: 'Test',
|
|
487
|
-
widgets: [
|
|
488
|
-
{
|
|
489
|
-
type: 'bar',
|
|
490
|
-
title: 'Mismatched yField',
|
|
491
|
-
object: 'opportunity',
|
|
492
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
493
|
-
options: {
|
|
494
|
-
xField: 'account',
|
|
495
|
-
yField: 'total',
|
|
496
|
-
data: {
|
|
497
|
-
provider: 'object',
|
|
498
|
-
object: 'opportunity',
|
|
499
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'account' },
|
|
500
|
-
},
|
|
501
|
-
},
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
} as any;
|
|
505
|
-
|
|
506
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
507
|
-
const schemas = getRenderedSchemas(container);
|
|
508
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
509
|
-
|
|
510
|
-
expect(chartSchema).toBeDefined();
|
|
511
|
-
// Even though yField is 'total', the series should use aggregate.field ('amount')
|
|
512
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'amount' }]);
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it('should produce object-chart schema for area chart with provider: object aggregate', () => {
|
|
516
|
-
const schema = {
|
|
517
|
-
type: 'dashboard' as const,
|
|
518
|
-
name: 'test',
|
|
519
|
-
title: 'Test',
|
|
520
|
-
widgets: [
|
|
521
|
-
{
|
|
522
|
-
type: 'area',
|
|
523
|
-
title: 'Revenue Trends',
|
|
524
|
-
object: 'opportunity',
|
|
525
|
-
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
526
|
-
options: {
|
|
527
|
-
xField: 'stage',
|
|
528
|
-
yField: 'expected_revenue',
|
|
529
|
-
data: {
|
|
530
|
-
provider: 'object',
|
|
531
|
-
object: 'opportunity',
|
|
532
|
-
aggregate: { field: 'expected_revenue', function: 'sum', groupBy: 'stage' },
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
],
|
|
537
|
-
} as any;
|
|
538
|
-
|
|
539
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
540
|
-
const schemas = getRenderedSchemas(container);
|
|
541
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
542
|
-
|
|
543
|
-
expect(chartSchema).toBeDefined();
|
|
544
|
-
expect(chartSchema.chartType).toBe('area');
|
|
545
|
-
expect(chartSchema.objectName).toBe('opportunity');
|
|
546
|
-
expect(chartSchema.aggregate).toEqual({
|
|
547
|
-
field: 'expected_revenue',
|
|
548
|
-
function: 'sum',
|
|
549
|
-
groupBy: 'stage',
|
|
550
|
-
});
|
|
551
|
-
expect(chartSchema.xAxisKey).toBe('stage');
|
|
552
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'expected_revenue' }]);
|
|
553
|
-
expect(chartSchema.data).toBeUndefined();
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('should produce object-chart schema for donut chart with count aggregate', () => {
|
|
557
|
-
const schema = {
|
|
558
|
-
type: 'dashboard' as const,
|
|
559
|
-
name: 'test',
|
|
560
|
-
title: 'Test',
|
|
561
|
-
widgets: [
|
|
562
|
-
{
|
|
563
|
-
type: 'donut',
|
|
564
|
-
title: 'Lead Source',
|
|
565
|
-
object: 'opportunity',
|
|
566
|
-
layout: { x: 0, y: 0, w: 1, h: 2 },
|
|
567
|
-
options: {
|
|
568
|
-
xField: 'lead_source',
|
|
569
|
-
yField: 'count',
|
|
570
|
-
data: {
|
|
571
|
-
provider: 'object',
|
|
572
|
-
object: 'opportunity',
|
|
573
|
-
aggregate: { field: 'count', function: 'count', groupBy: 'lead_source' },
|
|
574
|
-
},
|
|
575
|
-
},
|
|
576
|
-
},
|
|
577
|
-
],
|
|
578
|
-
} as any;
|
|
579
|
-
|
|
580
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
581
|
-
const schemas = getRenderedSchemas(container);
|
|
582
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
583
|
-
|
|
584
|
-
expect(chartSchema).toBeDefined();
|
|
585
|
-
expect(chartSchema.chartType).toBe('donut');
|
|
586
|
-
expect(chartSchema.objectName).toBe('opportunity');
|
|
587
|
-
expect(chartSchema.aggregate.function).toBe('count');
|
|
588
|
-
expect(chartSchema.xAxisKey).toBe('lead_source');
|
|
589
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'count' }]);
|
|
590
|
-
expect(chartSchema.data).toBeUndefined();
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
it('should produce object-chart schema for line chart with avg aggregate', () => {
|
|
594
|
-
const schema = {
|
|
595
|
-
type: 'dashboard' as const,
|
|
596
|
-
name: 'test',
|
|
597
|
-
title: 'Test',
|
|
598
|
-
widgets: [
|
|
599
|
-
{
|
|
600
|
-
type: 'line',
|
|
601
|
-
title: 'Avg Deal Size by Stage',
|
|
602
|
-
object: 'opportunity',
|
|
603
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
604
|
-
options: {
|
|
605
|
-
xField: 'stage',
|
|
606
|
-
yField: 'amount',
|
|
607
|
-
data: {
|
|
608
|
-
provider: 'object',
|
|
609
|
-
object: 'opportunity',
|
|
610
|
-
aggregate: { field: 'amount', function: 'avg', groupBy: 'stage' },
|
|
611
|
-
},
|
|
612
|
-
},
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
} as any;
|
|
616
|
-
|
|
617
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
618
|
-
const schemas = getRenderedSchemas(container);
|
|
619
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
620
|
-
|
|
621
|
-
expect(chartSchema).toBeDefined();
|
|
622
|
-
expect(chartSchema.chartType).toBe('line');
|
|
623
|
-
expect(chartSchema.aggregate.function).toBe('avg');
|
|
624
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'amount' }]);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it('should produce object-chart schema for cross-object widget (order)', () => {
|
|
628
|
-
const schema = {
|
|
629
|
-
type: 'dashboard' as const,
|
|
630
|
-
name: 'test',
|
|
631
|
-
title: 'Test',
|
|
632
|
-
widgets: [
|
|
633
|
-
{
|
|
634
|
-
type: 'bar',
|
|
635
|
-
title: 'Orders by Status',
|
|
636
|
-
object: 'order',
|
|
637
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
638
|
-
options: {
|
|
639
|
-
xField: 'status',
|
|
640
|
-
yField: 'amount',
|
|
641
|
-
data: {
|
|
642
|
-
provider: 'object',
|
|
643
|
-
object: 'order',
|
|
644
|
-
aggregate: { field: 'amount', function: 'max', groupBy: 'status' },
|
|
645
|
-
},
|
|
646
|
-
},
|
|
647
|
-
},
|
|
648
|
-
],
|
|
649
|
-
} as any;
|
|
650
|
-
|
|
651
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
652
|
-
const schemas = getRenderedSchemas(container);
|
|
653
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
654
|
-
|
|
655
|
-
expect(chartSchema).toBeDefined();
|
|
656
|
-
expect(chartSchema.chartType).toBe('bar');
|
|
657
|
-
expect(chartSchema.objectName).toBe('order');
|
|
658
|
-
expect(chartSchema.aggregate.function).toBe('max');
|
|
659
|
-
expect(chartSchema.xAxisKey).toBe('status');
|
|
660
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'amount' }]);
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
it('should render without errors when widgets array is empty', () => {
|
|
664
|
-
const schema = {
|
|
665
|
-
type: 'dashboard' as const,
|
|
666
|
-
name: 'test',
|
|
667
|
-
title: 'Empty Dashboard',
|
|
668
|
-
widgets: [],
|
|
669
|
-
} as any;
|
|
670
|
-
|
|
671
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
672
|
-
expect(container).toBeDefined();
|
|
673
|
-
expect(container.querySelectorAll('pre').length).toBe(0);
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
it('should handle chart widget with null data gracefully', () => {
|
|
677
|
-
const schema = {
|
|
678
|
-
type: 'dashboard' as const,
|
|
679
|
-
name: 'test',
|
|
680
|
-
title: 'Test',
|
|
681
|
-
widgets: [
|
|
682
|
-
{
|
|
683
|
-
type: 'bar',
|
|
684
|
-
title: 'Null Data Bar',
|
|
685
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
686
|
-
options: { xField: 'x', yField: 'y', data: null },
|
|
687
|
-
},
|
|
688
|
-
],
|
|
689
|
-
} as any;
|
|
690
|
-
|
|
691
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
692
|
-
const schemas = getRenderedSchemas(container);
|
|
693
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
694
|
-
|
|
695
|
-
expect(chartSchema).toBeDefined();
|
|
696
|
-
expect(chartSchema.data).toEqual([]);
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
it('should not crash data-table when provider:object leaks data config via options spread', () => {
|
|
700
|
-
const schema = {
|
|
701
|
-
type: 'dashboard' as const,
|
|
702
|
-
name: 'test',
|
|
703
|
-
title: 'Test',
|
|
704
|
-
widgets: [
|
|
705
|
-
{
|
|
706
|
-
type: 'table',
|
|
707
|
-
title: 'Provider Object Table',
|
|
708
|
-
object: 'opportunity',
|
|
709
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
710
|
-
options: {
|
|
711
|
-
columns: [
|
|
712
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
713
|
-
{ header: 'Amount', accessorKey: 'amount' },
|
|
714
|
-
],
|
|
715
|
-
data: {
|
|
716
|
-
provider: 'object',
|
|
717
|
-
object: 'opportunity',
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
},
|
|
721
|
-
],
|
|
722
|
-
} as any;
|
|
723
|
-
|
|
724
|
-
// Must render without throwing. Previously this crashed with
|
|
725
|
-
// "paginatedData.some is not a function" because the provider
|
|
726
|
-
// config object leaked through as data.
|
|
727
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
728
|
-
expect(container).toBeDefined();
|
|
729
|
-
// The component should not show a crash error
|
|
730
|
-
expect(container.textContent).not.toContain('is not a function');
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
it('should not crash pivot table when provider:object leaks data config via options spread', () => {
|
|
734
|
-
const schema = {
|
|
735
|
-
type: 'dashboard' as const,
|
|
736
|
-
name: 'test',
|
|
737
|
-
title: 'Test',
|
|
738
|
-
widgets: [
|
|
739
|
-
{
|
|
740
|
-
type: 'pivot',
|
|
741
|
-
title: 'Provider Object Pivot',
|
|
742
|
-
object: 'sales',
|
|
743
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
744
|
-
options: {
|
|
745
|
-
rowField: 'region',
|
|
746
|
-
columnField: 'quarter',
|
|
747
|
-
valueField: 'revenue',
|
|
748
|
-
data: {
|
|
749
|
-
provider: 'object',
|
|
750
|
-
object: 'sales',
|
|
751
|
-
},
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
],
|
|
755
|
-
} as any;
|
|
756
|
-
|
|
757
|
-
// Must render without throwing. Previously this crashed with
|
|
758
|
-
// "data is not iterable" because the provider config object
|
|
759
|
-
// leaked through as data.
|
|
760
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
761
|
-
expect(container).toBeDefined();
|
|
762
|
-
expect(container.textContent).not.toContain('is not iterable');
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
it('should handle scatter chart type as a valid chart widget', () => {
|
|
766
|
-
const schema = {
|
|
767
|
-
type: 'dashboard' as const,
|
|
768
|
-
name: 'test',
|
|
769
|
-
title: 'Test',
|
|
770
|
-
widgets: [
|
|
771
|
-
{
|
|
772
|
-
type: 'scatter',
|
|
773
|
-
title: 'Scatter Plot',
|
|
774
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
775
|
-
options: {
|
|
776
|
-
xField: 'x',
|
|
777
|
-
yField: 'y',
|
|
778
|
-
data: {
|
|
779
|
-
provider: 'value',
|
|
780
|
-
items: [
|
|
781
|
-
{ x: 1, y: 10 },
|
|
782
|
-
{ x: 2, y: 20 },
|
|
783
|
-
],
|
|
784
|
-
},
|
|
785
|
-
},
|
|
786
|
-
},
|
|
787
|
-
],
|
|
788
|
-
} as any;
|
|
789
|
-
|
|
790
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
791
|
-
const schemas = getRenderedSchemas(container);
|
|
792
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
793
|
-
|
|
794
|
-
expect(chartSchema).toBeDefined();
|
|
795
|
-
expect(chartSchema.chartType).toBe('scatter');
|
|
796
|
-
expect(chartSchema.data).toHaveLength(2);
|
|
797
|
-
expect(chartSchema.xAxisKey).toBe('x');
|
|
798
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'y' }]);
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
it('should produce object-chart schema for scatter chart with provider: object', () => {
|
|
802
|
-
const schema = {
|
|
803
|
-
type: 'dashboard' as const,
|
|
804
|
-
name: 'test',
|
|
805
|
-
title: 'Test',
|
|
806
|
-
widgets: [
|
|
807
|
-
{
|
|
808
|
-
type: 'scatter',
|
|
809
|
-
title: 'Object Scatter',
|
|
810
|
-
object: 'opportunity',
|
|
811
|
-
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
812
|
-
options: {
|
|
813
|
-
xField: 'amount',
|
|
814
|
-
yField: 'probability',
|
|
815
|
-
data: {
|
|
816
|
-
provider: 'object',
|
|
817
|
-
object: 'opportunity',
|
|
818
|
-
aggregate: { field: 'probability', function: 'avg', groupBy: 'amount' },
|
|
819
|
-
},
|
|
820
|
-
},
|
|
821
|
-
},
|
|
822
|
-
],
|
|
823
|
-
} as any;
|
|
824
|
-
|
|
825
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
826
|
-
const schemas = getRenderedSchemas(container);
|
|
827
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
828
|
-
|
|
829
|
-
expect(chartSchema).toBeDefined();
|
|
830
|
-
expect(chartSchema.chartType).toBe('scatter');
|
|
831
|
-
expect(chartSchema.objectName).toBe('opportunity');
|
|
832
|
-
expect(chartSchema.aggregate.function).toBe('avg');
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
it('should use widget.categoryField as xAxisKey fallback over options.xField', () => {
|
|
836
|
-
const schema = {
|
|
837
|
-
type: 'dashboard' as const,
|
|
838
|
-
name: 'test',
|
|
839
|
-
title: 'Test',
|
|
840
|
-
widgets: [
|
|
841
|
-
{
|
|
842
|
-
type: 'bar',
|
|
843
|
-
title: 'Category Field Override',
|
|
844
|
-
categoryField: 'forecast_category',
|
|
845
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
846
|
-
options: {
|
|
847
|
-
xField: 'stage',
|
|
848
|
-
yField: 'amount',
|
|
849
|
-
data: {
|
|
850
|
-
provider: 'value',
|
|
851
|
-
items: [
|
|
852
|
-
{ forecast_category: 'Pipeline', amount: 100 },
|
|
853
|
-
{ forecast_category: 'Closed', amount: 200 },
|
|
854
|
-
],
|
|
855
|
-
},
|
|
856
|
-
},
|
|
857
|
-
},
|
|
858
|
-
],
|
|
859
|
-
} as any;
|
|
860
|
-
|
|
861
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
862
|
-
const schemas = getRenderedSchemas(container);
|
|
863
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
864
|
-
|
|
865
|
-
expect(chartSchema).toBeDefined();
|
|
866
|
-
// widget.categoryField should override options.xField
|
|
867
|
-
expect(chartSchema.xAxisKey).toBe('forecast_category');
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
it('should use widget.valueField as yField fallback over options.yField', () => {
|
|
871
|
-
const schema = {
|
|
872
|
-
type: 'dashboard' as const,
|
|
873
|
-
name: 'test',
|
|
874
|
-
title: 'Test',
|
|
875
|
-
widgets: [
|
|
876
|
-
{
|
|
877
|
-
type: 'line',
|
|
878
|
-
title: 'Value Field Override',
|
|
879
|
-
valueField: 'expected_revenue',
|
|
880
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
881
|
-
options: {
|
|
882
|
-
xField: 'month',
|
|
883
|
-
yField: 'amount',
|
|
884
|
-
data: {
|
|
885
|
-
provider: 'value',
|
|
886
|
-
items: [
|
|
887
|
-
{ month: 'Jan', expected_revenue: 100 },
|
|
888
|
-
],
|
|
889
|
-
},
|
|
890
|
-
},
|
|
891
|
-
},
|
|
892
|
-
],
|
|
893
|
-
} as any;
|
|
894
|
-
|
|
895
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
896
|
-
const schemas = getRenderedSchemas(container);
|
|
897
|
-
const chartSchema = schemas.find(s => s.type === 'chart');
|
|
898
|
-
|
|
899
|
-
expect(chartSchema).toBeDefined();
|
|
900
|
-
// widget.valueField should override options.yField
|
|
901
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'expected_revenue' }]);
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
it('should construct object-chart from widget-level fields when no data provider exists', () => {
|
|
905
|
-
const schema = {
|
|
906
|
-
type: 'dashboard' as const,
|
|
907
|
-
name: 'test',
|
|
908
|
-
title: 'Test',
|
|
909
|
-
widgets: [
|
|
910
|
-
{
|
|
911
|
-
type: 'bar',
|
|
912
|
-
title: 'New Widget',
|
|
913
|
-
object: 'opportunity',
|
|
914
|
-
categoryField: 'stage',
|
|
915
|
-
valueField: 'amount',
|
|
916
|
-
aggregate: 'sum',
|
|
917
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
918
|
-
},
|
|
919
|
-
],
|
|
920
|
-
} as any;
|
|
921
|
-
|
|
922
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
923
|
-
const schemas = getRenderedSchemas(container);
|
|
924
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
925
|
-
|
|
926
|
-
expect(chartSchema).toBeDefined();
|
|
927
|
-
expect(chartSchema.chartType).toBe('bar');
|
|
928
|
-
expect(chartSchema.objectName).toBe('opportunity');
|
|
929
|
-
expect(chartSchema.xAxisKey).toBe('stage');
|
|
930
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'amount' }]);
|
|
931
|
-
expect(chartSchema.aggregate).toEqual({
|
|
932
|
-
field: 'amount',
|
|
933
|
-
function: 'sum',
|
|
934
|
-
groupBy: 'stage',
|
|
935
|
-
});
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
it('should construct data-table from widget.object when no data provider exists', () => {
|
|
939
|
-
const schema = {
|
|
940
|
-
type: 'dashboard' as const,
|
|
941
|
-
name: 'test',
|
|
942
|
-
title: 'Test',
|
|
943
|
-
widgets: [
|
|
944
|
-
{
|
|
945
|
-
type: 'table',
|
|
946
|
-
title: 'New Table Widget',
|
|
947
|
-
object: 'contact',
|
|
948
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
} as any;
|
|
952
|
-
|
|
953
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
954
|
-
const schemas = getRenderedSchemas(container);
|
|
955
|
-
// DashboardRenderer now routes table+objectName to 'object-data-table'
|
|
956
|
-
const tableSchema = schemas.find(s => s.type === 'object-data-table');
|
|
957
|
-
|
|
958
|
-
if (tableSchema) {
|
|
959
|
-
expect(tableSchema.objectName).toBe('contact');
|
|
960
|
-
}
|
|
961
|
-
// Either way, it should not crash
|
|
962
|
-
expect(container).toBeDefined();
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
// ---- Live preview: widget-level fields override data provider config ------
|
|
966
|
-
|
|
967
|
-
it('should override data provider aggregate.groupBy with widget.categoryField', () => {
|
|
968
|
-
const schema = {
|
|
969
|
-
type: 'dashboard' as const,
|
|
970
|
-
name: 'test',
|
|
971
|
-
title: 'Test',
|
|
972
|
-
widgets: [
|
|
973
|
-
{
|
|
974
|
-
type: 'bar',
|
|
975
|
-
title: 'Live Preview',
|
|
976
|
-
object: 'opportunity',
|
|
977
|
-
categoryField: 'region',
|
|
978
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
979
|
-
options: {
|
|
980
|
-
xField: 'stage',
|
|
981
|
-
yField: 'amount',
|
|
982
|
-
data: {
|
|
983
|
-
provider: 'object',
|
|
984
|
-
object: 'opportunity',
|
|
985
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
986
|
-
},
|
|
987
|
-
},
|
|
988
|
-
},
|
|
989
|
-
],
|
|
990
|
-
} as any;
|
|
991
|
-
|
|
992
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
993
|
-
const schemas = getRenderedSchemas(container);
|
|
994
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
995
|
-
|
|
996
|
-
expect(chartSchema).toBeDefined();
|
|
997
|
-
// widget.categoryField ('region') should override aggregate.groupBy ('stage')
|
|
998
|
-
expect(chartSchema.aggregate.groupBy).toBe('region');
|
|
999
|
-
expect(chartSchema.xAxisKey).toBe('region');
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
it('should override data provider aggregate.field with widget.valueField', () => {
|
|
1003
|
-
const schema = {
|
|
1004
|
-
type: 'dashboard' as const,
|
|
1005
|
-
name: 'test',
|
|
1006
|
-
title: 'Test',
|
|
1007
|
-
widgets: [
|
|
1008
|
-
{
|
|
1009
|
-
type: 'area',
|
|
1010
|
-
title: 'Live Preview',
|
|
1011
|
-
object: 'opportunity',
|
|
1012
|
-
valueField: 'expected_revenue',
|
|
1013
|
-
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
1014
|
-
options: {
|
|
1015
|
-
xField: 'stage',
|
|
1016
|
-
yField: 'amount',
|
|
1017
|
-
data: {
|
|
1018
|
-
provider: 'object',
|
|
1019
|
-
object: 'opportunity',
|
|
1020
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
1021
|
-
},
|
|
1022
|
-
},
|
|
1023
|
-
},
|
|
1024
|
-
],
|
|
1025
|
-
} as any;
|
|
1026
|
-
|
|
1027
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1028
|
-
const schemas = getRenderedSchemas(container);
|
|
1029
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
1030
|
-
|
|
1031
|
-
expect(chartSchema).toBeDefined();
|
|
1032
|
-
// widget.valueField ('expected_revenue') should override aggregate.field ('amount')
|
|
1033
|
-
expect(chartSchema.aggregate.field).toBe('expected_revenue');
|
|
1034
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'expected_revenue' }]);
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
it('should override data provider aggregate.function with widget.aggregate', () => {
|
|
1038
|
-
const schema = {
|
|
1039
|
-
type: 'dashboard' as const,
|
|
1040
|
-
name: 'test',
|
|
1041
|
-
title: 'Test',
|
|
1042
|
-
widgets: [
|
|
1043
|
-
{
|
|
1044
|
-
type: 'bar',
|
|
1045
|
-
title: 'Live Preview',
|
|
1046
|
-
object: 'opportunity',
|
|
1047
|
-
aggregate: 'count',
|
|
1048
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
1049
|
-
options: {
|
|
1050
|
-
xField: 'stage',
|
|
1051
|
-
yField: 'amount',
|
|
1052
|
-
data: {
|
|
1053
|
-
provider: 'object',
|
|
1054
|
-
object: 'opportunity',
|
|
1055
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
1056
|
-
},
|
|
1057
|
-
},
|
|
1058
|
-
},
|
|
1059
|
-
],
|
|
1060
|
-
} as any;
|
|
1061
|
-
|
|
1062
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1063
|
-
const schemas = getRenderedSchemas(container);
|
|
1064
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
1065
|
-
|
|
1066
|
-
expect(chartSchema).toBeDefined();
|
|
1067
|
-
// widget.aggregate ('count') should override aggregate.function ('sum')
|
|
1068
|
-
expect(chartSchema.aggregate.function).toBe('count');
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
it('should prefer widget.object over data provider object for objectName', () => {
|
|
1072
|
-
const schema = {
|
|
1073
|
-
type: 'dashboard' as const,
|
|
1074
|
-
name: 'test',
|
|
1075
|
-
title: 'Test',
|
|
1076
|
-
widgets: [
|
|
1077
|
-
{
|
|
1078
|
-
type: 'line',
|
|
1079
|
-
title: 'Live Preview',
|
|
1080
|
-
object: 'contact',
|
|
1081
|
-
layout: { x: 0, y: 0, w: 3, h: 2 },
|
|
1082
|
-
options: {
|
|
1083
|
-
xField: 'month',
|
|
1084
|
-
yField: 'count',
|
|
1085
|
-
data: {
|
|
1086
|
-
provider: 'object',
|
|
1087
|
-
object: 'opportunity',
|
|
1088
|
-
aggregate: { field: 'count', function: 'count', groupBy: 'month' },
|
|
1089
|
-
},
|
|
1090
|
-
},
|
|
1091
|
-
},
|
|
1092
|
-
],
|
|
1093
|
-
} as any;
|
|
1094
|
-
|
|
1095
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1096
|
-
const schemas = getRenderedSchemas(container);
|
|
1097
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
1098
|
-
|
|
1099
|
-
expect(chartSchema).toBeDefined();
|
|
1100
|
-
// widget.object ('contact') should override data.object ('opportunity')
|
|
1101
|
-
expect(chartSchema.objectName).toBe('contact');
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
it('should prefer widget.object for table widgets with data provider', () => {
|
|
1105
|
-
const schema = {
|
|
1106
|
-
type: 'dashboard' as const,
|
|
1107
|
-
name: 'test',
|
|
1108
|
-
title: 'Test',
|
|
1109
|
-
widgets: [
|
|
1110
|
-
{
|
|
1111
|
-
type: 'table',
|
|
1112
|
-
title: 'Live Preview Table',
|
|
1113
|
-
object: 'contact',
|
|
1114
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
1115
|
-
options: {
|
|
1116
|
-
data: {
|
|
1117
|
-
provider: 'object',
|
|
1118
|
-
object: 'opportunity',
|
|
1119
|
-
},
|
|
1120
|
-
},
|
|
1121
|
-
},
|
|
1122
|
-
],
|
|
1123
|
-
} as any;
|
|
1124
|
-
|
|
1125
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1126
|
-
const schemas = getRenderedSchemas(container);
|
|
1127
|
-
const tableSchema = schemas.find(s => s.type === 'data-table');
|
|
1128
|
-
|
|
1129
|
-
if (tableSchema) {
|
|
1130
|
-
// widget.object ('contact') should override data.object ('opportunity')
|
|
1131
|
-
expect(tableSchema.objectName).toBe('contact');
|
|
1132
|
-
}
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
it('should apply all widget-level field overrides simultaneously for live preview', () => {
|
|
1136
|
-
const schema = {
|
|
1137
|
-
type: 'dashboard' as const,
|
|
1138
|
-
name: 'test',
|
|
1139
|
-
title: 'Test',
|
|
1140
|
-
widgets: [
|
|
1141
|
-
{
|
|
1142
|
-
type: 'pie',
|
|
1143
|
-
title: 'Full Override',
|
|
1144
|
-
object: 'account',
|
|
1145
|
-
categoryField: 'industry',
|
|
1146
|
-
valueField: 'revenue',
|
|
1147
|
-
aggregate: 'avg',
|
|
1148
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
1149
|
-
options: {
|
|
1150
|
-
xField: 'stage',
|
|
1151
|
-
yField: 'amount',
|
|
1152
|
-
data: {
|
|
1153
|
-
provider: 'object',
|
|
1154
|
-
object: 'opportunity',
|
|
1155
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
1156
|
-
},
|
|
1157
|
-
},
|
|
1158
|
-
},
|
|
1159
|
-
],
|
|
1160
|
-
} as any;
|
|
1161
|
-
|
|
1162
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1163
|
-
const schemas = getRenderedSchemas(container);
|
|
1164
|
-
const chartSchema = schemas.find(s => s.type === 'object-chart');
|
|
1165
|
-
|
|
1166
|
-
expect(chartSchema).toBeDefined();
|
|
1167
|
-
expect(chartSchema.chartType).toBe('pie');
|
|
1168
|
-
expect(chartSchema.objectName).toBe('account');
|
|
1169
|
-
expect(chartSchema.xAxisKey).toBe('industry');
|
|
1170
|
-
expect(chartSchema.aggregate).toEqual({
|
|
1171
|
-
field: 'revenue',
|
|
1172
|
-
function: 'avg',
|
|
1173
|
-
groupBy: 'industry',
|
|
1174
|
-
});
|
|
1175
|
-
expect(chartSchema.series).toEqual([{ dataKey: 'revenue' }]);
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
// ---- Pivot widget: object binding without explicit data provider ----------
|
|
1179
|
-
|
|
1180
|
-
it('should pass objectName for pivot widget with widget.object but no data', () => {
|
|
1181
|
-
const schema = {
|
|
1182
|
-
type: 'dashboard' as const,
|
|
1183
|
-
name: 'test',
|
|
1184
|
-
title: 'Test',
|
|
1185
|
-
widgets: [
|
|
1186
|
-
{
|
|
1187
|
-
type: 'pivot',
|
|
1188
|
-
title: 'Pivot by Object',
|
|
1189
|
-
object: 'sales',
|
|
1190
|
-
layout: { x: 0, y: 0, w: 4, h: 2 },
|
|
1191
|
-
options: {
|
|
1192
|
-
rowField: 'region',
|
|
1193
|
-
columnField: 'quarter',
|
|
1194
|
-
valueField: 'revenue',
|
|
1195
|
-
},
|
|
1196
|
-
},
|
|
1197
|
-
],
|
|
1198
|
-
} as any;
|
|
1199
|
-
|
|
1200
|
-
// DashboardRenderer routes pivot+objectName to 'object-pivot' type.
|
|
1201
|
-
// ObjectPivotTable renders "no data source" message when no context provided.
|
|
1202
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1203
|
-
expect(container).toBeDefined();
|
|
1204
|
-
// Should render without crash
|
|
1205
|
-
expect(container.textContent).not.toContain('is not iterable');
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
|
-
// ---- Widget description rendering -----------------------------------------
|
|
1209
|
-
|
|
1210
|
-
it('should render widget description in card header', () => {
|
|
1211
|
-
const schema = {
|
|
1212
|
-
type: 'dashboard' as const,
|
|
1213
|
-
name: 'test',
|
|
1214
|
-
title: 'Test',
|
|
1215
|
-
widgets: [
|
|
1216
|
-
{
|
|
1217
|
-
type: 'bar',
|
|
1218
|
-
title: 'My Chart',
|
|
1219
|
-
description: 'Monthly sales breakdown',
|
|
1220
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
1221
|
-
options: {
|
|
1222
|
-
data: { provider: 'value', items: [{ name: 'A', value: 100 }] },
|
|
1223
|
-
},
|
|
1224
|
-
},
|
|
1225
|
-
],
|
|
1226
|
-
} as any;
|
|
1227
|
-
|
|
1228
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1229
|
-
expect(container.textContent).toContain('Monthly sales breakdown');
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
it('should resolve I18nLabel description in widget card', () => {
|
|
1233
|
-
const schema = {
|
|
1234
|
-
type: 'dashboard' as const,
|
|
1235
|
-
name: 'test',
|
|
1236
|
-
title: 'Test',
|
|
1237
|
-
widgets: [
|
|
1238
|
-
{
|
|
1239
|
-
type: 'bar',
|
|
1240
|
-
title: 'My Chart',
|
|
1241
|
-
description: { key: 'desc.key', defaultValue: 'Resolved description' },
|
|
1242
|
-
layout: { x: 0, y: 0, w: 2, h: 2 },
|
|
1243
|
-
options: {
|
|
1244
|
-
data: { provider: 'value', items: [{ name: 'A', value: 100 }] },
|
|
1245
|
-
},
|
|
1246
|
-
},
|
|
1247
|
-
],
|
|
1248
|
-
} as any;
|
|
1249
|
-
|
|
1250
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1251
|
-
expect(container.textContent).toContain('Resolved description');
|
|
1252
|
-
expect(container.textContent).not.toContain('[object Object]');
|
|
1253
|
-
});
|
|
1254
|
-
|
|
1255
|
-
// ---- Grid column clamping -------------------------------------------------
|
|
1256
|
-
|
|
1257
|
-
it('should clamp widget grid span to dashboard columns', () => {
|
|
1258
|
-
const schema = {
|
|
1259
|
-
type: 'dashboard' as const,
|
|
1260
|
-
name: 'test',
|
|
1261
|
-
title: 'Test',
|
|
1262
|
-
columns: 3,
|
|
1263
|
-
widgets: [
|
|
1264
|
-
{
|
|
1265
|
-
type: 'bar',
|
|
1266
|
-
title: 'Wide Chart',
|
|
1267
|
-
layout: { x: 0, y: 0, w: 6, h: 2 },
|
|
1268
|
-
options: {
|
|
1269
|
-
data: { provider: 'value', items: [{ name: 'A', value: 100 }] },
|
|
1270
|
-
},
|
|
1271
|
-
},
|
|
1272
|
-
],
|
|
1273
|
-
} as any;
|
|
1274
|
-
|
|
1275
|
-
const { container } = render(<DashboardRenderer schema={schema} />);
|
|
1276
|
-
// The card's gridColumn should be clamped to 3, not 6
|
|
1277
|
-
const card = container.querySelector('[class*="overflow-hidden"]');
|
|
1278
|
-
expect(card).toBeDefined();
|
|
1279
|
-
if (card) {
|
|
1280
|
-
expect((card as HTMLElement).style.gridColumn).toBe('span 3');
|
|
1281
|
-
}
|
|
1282
|
-
});
|
|
1283
|
-
});
|