@object-ui/plugin-dashboard 3.0.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +40 -7
- package/dist/index.js +3848 -2635
- package/dist/index.umd.cjs +5 -5
- package/dist/src/DashboardConfigPanel.d.ts +28 -0
- package/dist/src/DashboardConfigPanel.d.ts.map +1 -0
- package/dist/src/DashboardConfigPanel.stories.d.ts +14 -0
- package/dist/src/DashboardConfigPanel.stories.d.ts.map +1 -0
- package/dist/src/DashboardGridLayout.d.ts.map +1 -1
- package/dist/src/DashboardRenderer.d.ts +14 -0
- package/dist/src/DashboardRenderer.d.ts.map +1 -1
- package/dist/src/DashboardWithConfig.d.ts +32 -0
- package/dist/src/DashboardWithConfig.d.ts.map +1 -0
- package/dist/src/MetricCard.d.ts +8 -2
- package/dist/src/MetricCard.d.ts.map +1 -1
- package/dist/src/MetricWidget.d.ts +12 -3
- package/dist/src/MetricWidget.d.ts.map +1 -1
- package/dist/src/ObjectDataTable.d.ts +39 -0
- package/dist/src/ObjectDataTable.d.ts.map +1 -0
- package/dist/src/ObjectPivotTable.d.ts +29 -0
- package/dist/src/ObjectPivotTable.d.ts.map +1 -0
- package/dist/src/PivotTable.d.ts +14 -0
- package/dist/src/PivotTable.d.ts.map +1 -0
- package/dist/src/WidgetConfigPanel.d.ts +43 -0
- package/dist/src/WidgetConfigPanel.d.ts.map +1 -0
- package/dist/src/index.d.ts +13 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/utils.d.ts +14 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/DashboardConfigPanel.stories.tsx +164 -0
- package/src/DashboardConfigPanel.tsx +158 -0
- package/src/DashboardGridLayout.tsx +101 -3
- package/src/DashboardRenderer.tsx +269 -28
- package/src/DashboardWithConfig.tsx +211 -0
- package/src/MetricCard.tsx +11 -4
- package/src/MetricWidget.tsx +18 -11
- package/src/ObjectDataTable.tsx +191 -0
- package/src/ObjectPivotTable.tsx +160 -0
- package/src/PivotTable.tsx +262 -0
- package/src/WidgetConfigPanel.tsx +540 -0
- package/src/__tests__/DashboardConfigPanel.test.tsx +206 -0
- package/src/__tests__/DashboardRenderer.designMode.test.tsx +386 -0
- package/src/__tests__/DashboardRenderer.header.test.tsx +114 -0
- package/src/__tests__/DashboardRenderer.mobile.test.tsx +214 -0
- package/src/__tests__/DashboardRenderer.widgetData.test.tsx +1022 -0
- package/src/__tests__/DashboardWithConfig.test.tsx +276 -0
- package/src/__tests__/MetricCard.test.tsx +23 -0
- package/src/__tests__/ObjectDataTable.test.tsx +122 -0
- package/src/__tests__/ObjectPivotTable.test.tsx +192 -0
- package/src/__tests__/PivotTable.test.tsx +162 -0
- package/src/__tests__/WidgetConfigPanel.test.tsx +492 -0
- package/src/__tests__/ensureWidgetIds.test.tsx +103 -0
- package/src/index.tsx +107 -1
- package/src/utils.ts +17 -0
|
@@ -224,6 +224,45 @@ describe('DashboardRenderer widget data extraction', () => {
|
|
|
224
224
|
expect(container.textContent).toContain('5');
|
|
225
225
|
});
|
|
226
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
|
+
|
|
227
266
|
it('should assign unique keys to widgets without id or title', () => {
|
|
228
267
|
const schema = {
|
|
229
268
|
type: 'dashboard' as const,
|
|
@@ -258,4 +297,987 @@ describe('DashboardRenderer widget data extraction', () => {
|
|
|
258
297
|
expect(container.textContent).toContain('200');
|
|
259
298
|
expect(container.textContent).toContain('300');
|
|
260
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
|
+
});
|
|
261
1283
|
});
|