Skip to content

Commit bf04728

Browse files
authored
Merge pull request #720 from objectstack-ai/copilot/enhance-dashboardwidget-types
2 parents 2d9a88e + 4888904 commit bf04728

2 files changed

Lines changed: 165 additions & 3 deletions

File tree

ROADMAP.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,10 @@ Protocol enhancements and core component implementations for dashboard feature p
418418
Advanced widget types and chart implementations.
419419

420420
- [ ] Implement `PivotTable` component with row/column totals and multi-measure support ([objectui#585](https://github.com/objectstack-ai/objectui/issues/585))
421-
- [ ] Implement `funnel` chart type in `plugin-charts` ([#713](https://github.com/objectstack-ai/spec/issues/713))
422-
- [ ] Implement `grouped-bar` chart type in `plugin-charts` ([#713](https://github.com/objectstack-ai/spec/issues/713))
421+
- [x] Protocol support for `funnel` chart type in `DashboardWidgetSchema` ([#713](https://github.com/objectstack-ai/spec/issues/713))
422+
- [x] Protocol support for `grouped-bar` chart type in `DashboardWidgetSchema` ([#713](https://github.com/objectstack-ai/spec/issues/713))
423+
- [ ] Implement `funnel` chart renderer in `plugin-charts` ([#713](https://github.com/objectstack-ai/spec/issues/713))
424+
- [ ] Implement `grouped-bar` chart renderer in `plugin-charts` ([#713](https://github.com/objectstack-ai/spec/issues/713))
423425
- [ ] Implement `stacked-bar` chart type in `plugin-charts` ([#713](https://github.com/objectstack-ai/spec/issues/713))
424426
- [ ] Implement `horizontal-bar` chart variant in `plugin-charts` ([#713](https://github.com/objectstack-ai/spec/issues/713))
425427

packages/spec/src/ui/dashboard.test.ts

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ChartTypeSchema } from './chart.zod';
2222

2323
describe('ChartTypeSchema', () => {
2424
it('should accept all chart types', () => {
25-
const types = ['metric', 'bar', 'line', 'pie', 'funnel', 'table', 'bubble', 'gauge', 'heatmap'];
25+
const types = ['metric', 'bar', 'line', 'pie', 'funnel', 'table', 'bubble', 'gauge', 'heatmap', 'pivot', 'grouped-bar'];
2626

2727
types.forEach(type => {
2828
expect(() => ChartTypeSchema.parse(type)).not.toThrow();
@@ -104,6 +104,54 @@ describe('DashboardWidgetSchema', () => {
104104
expect(() => DashboardWidgetSchema.parse(widget)).not.toThrow();
105105
});
106106

107+
it('should accept pivot widget', () => {
108+
const widget: DashboardWidget = {
109+
title: 'Revenue by Region × Product',
110+
type: 'pivot',
111+
object: 'order',
112+
categoryField: 'region',
113+
measures: [
114+
{ valueField: 'revenue', aggregate: 'sum', label: 'Total Revenue', format: '$0,0' },
115+
{ valueField: 'quantity', aggregate: 'sum', label: 'Units Sold' },
116+
],
117+
layout: { x: 0, y: 0, w: 12, h: 6 },
118+
};
119+
120+
const result = DashboardWidgetSchema.parse(widget);
121+
expect(result.type).toBe('pivot');
122+
expect(result.measures).toHaveLength(2);
123+
});
124+
125+
it('should accept funnel widget', () => {
126+
const widget: DashboardWidget = {
127+
title: 'Sales Funnel',
128+
type: 'funnel',
129+
object: 'opportunity',
130+
categoryField: 'stage',
131+
valueField: 'amount',
132+
aggregate: 'sum',
133+
layout: { x: 0, y: 0, w: 6, h: 4 },
134+
};
135+
136+
const result = DashboardWidgetSchema.parse(widget);
137+
expect(result.type).toBe('funnel');
138+
});
139+
140+
it('should accept grouped-bar widget', () => {
141+
const widget: DashboardWidget = {
142+
title: 'Quarterly Revenue by Region',
143+
type: 'grouped-bar',
144+
object: 'order',
145+
categoryField: 'quarter',
146+
valueField: 'revenue',
147+
aggregate: 'sum',
148+
layout: { x: 0, y: 0, w: 12, h: 4 },
149+
};
150+
151+
const result = DashboardWidgetSchema.parse(widget);
152+
expect(result.type).toBe('grouped-bar');
153+
});
154+
107155
it('should accept table widget', () => {
108156
const widget: DashboardWidget = {
109157
title: 'Top Accounts',
@@ -1437,3 +1485,115 @@ describe('DashboardWidgetSchema - measures (multi-measure pivot)', () => {
14371485
expect(dashboard.widgets[1].measures![3].valueField).toBe('margin');
14381486
});
14391487
});
1488+
1489+
// ============================================================================
1490+
// Protocol Enhancement Tests: pivot / funnel / grouped-bar widget types (#713)
1491+
// ============================================================================
1492+
1493+
describe('DashboardWidgetSchema - pivot/funnel/grouped-bar types', () => {
1494+
it('should accept funnel widget with chartConfig', () => {
1495+
const widget = DashboardWidgetSchema.parse({
1496+
title: 'Lead Conversion Funnel',
1497+
type: 'funnel',
1498+
object: 'lead',
1499+
categoryField: 'stage',
1500+
aggregate: 'count',
1501+
chartConfig: {
1502+
type: 'funnel',
1503+
showDataLabels: true,
1504+
colors: ['#4CAF50', '#FF9800', '#F44336'],
1505+
},
1506+
layout: { x: 0, y: 0, w: 6, h: 4 },
1507+
});
1508+
expect(widget.type).toBe('funnel');
1509+
expect(widget.chartConfig!.type).toBe('funnel');
1510+
expect(widget.chartConfig!.showDataLabels).toBe(true);
1511+
});
1512+
1513+
it('should accept grouped-bar widget with chartConfig', () => {
1514+
const widget = DashboardWidgetSchema.parse({
1515+
title: 'Revenue by Region & Quarter',
1516+
type: 'grouped-bar',
1517+
object: 'order',
1518+
categoryField: 'region',
1519+
valueField: 'revenue',
1520+
aggregate: 'sum',
1521+
chartConfig: {
1522+
type: 'grouped-bar',
1523+
showLegend: true,
1524+
showDataLabels: false,
1525+
xAxis: { field: 'region', title: 'Region' },
1526+
yAxis: [{ field: 'revenue', title: 'Revenue ($)', format: '$0,0' }],
1527+
},
1528+
layout: { x: 0, y: 0, w: 12, h: 4 },
1529+
});
1530+
expect(widget.type).toBe('grouped-bar');
1531+
expect(widget.chartConfig!.type).toBe('grouped-bar');
1532+
expect(widget.chartConfig!.showLegend).toBe(true);
1533+
});
1534+
1535+
it('should accept pivot widget with measures and chartConfig', () => {
1536+
const widget = DashboardWidgetSchema.parse({
1537+
title: 'Sales Cross-Tab Analysis',
1538+
type: 'pivot',
1539+
object: 'opportunity',
1540+
categoryField: 'region',
1541+
measures: [
1542+
{ valueField: 'amount', aggregate: 'sum', label: 'Total', format: '$0,0' },
1543+
{ valueField: 'amount', aggregate: 'count', label: 'Count' },
1544+
],
1545+
chartConfig: {
1546+
type: 'pivot',
1547+
showDataLabels: true,
1548+
},
1549+
layout: { x: 0, y: 0, w: 12, h: 6 },
1550+
});
1551+
expect(widget.type).toBe('pivot');
1552+
expect(widget.measures).toHaveLength(2);
1553+
});
1554+
1555+
it('should accept dashboard with pivot, funnel, and grouped-bar widgets', () => {
1556+
const dashboard = Dashboard.create({
1557+
name: 'analytics_overview',
1558+
label: 'Analytics Overview',
1559+
description: 'Dashboard combining pivot, funnel, and grouped-bar widgets',
1560+
widgets: [
1561+
{
1562+
title: 'Sales Funnel',
1563+
type: 'funnel',
1564+
object: 'lead',
1565+
categoryField: 'stage',
1566+
aggregate: 'count',
1567+
layout: { x: 0, y: 0, w: 6, h: 4 },
1568+
},
1569+
{
1570+
title: 'Revenue by Region & Quarter',
1571+
type: 'grouped-bar',
1572+
object: 'order',
1573+
categoryField: 'region',
1574+
valueField: 'revenue',
1575+
aggregate: 'sum',
1576+
layout: { x: 6, y: 0, w: 6, h: 4 },
1577+
},
1578+
{
1579+
title: 'Regional Pivot Analysis',
1580+
type: 'pivot',
1581+
object: 'opportunity',
1582+
categoryField: 'region',
1583+
measures: [
1584+
{ valueField: 'amount', aggregate: 'sum', label: 'Revenue', format: '$0,0' },
1585+
{ valueField: 'amount', aggregate: 'avg', label: 'Avg Deal', format: '$0,0.00' },
1586+
{ valueField: 'amount', aggregate: 'count', label: 'Deals' },
1587+
],
1588+
layout: { x: 0, y: 4, w: 12, h: 6 },
1589+
},
1590+
],
1591+
});
1592+
1593+
expect(dashboard.widgets).toHaveLength(3);
1594+
expect(dashboard.widgets[0].type).toBe('funnel');
1595+
expect(dashboard.widgets[1].type).toBe('grouped-bar');
1596+
expect(dashboard.widgets[2].type).toBe('pivot');
1597+
expect(dashboard.widgets[2].measures).toHaveLength(3);
1598+
});
1599+
});

0 commit comments

Comments
 (0)