Skip to content

Commit b14967b

Browse files
Copilothotlong
andcommitted
feat: add colorVariant, description, actionUrl/actionType/actionIcon to DashboardWidgetSchema
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent fb07f57 commit b14967b

3 files changed

Lines changed: 283 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ The following renames are planned for packages that implement core service contr
401401
Protocol enhancements and core component implementations for dashboard feature parity.
402402

403403
**Spec Protocol Changes:**
404-
- [ ] Add `colorVariant`, `actionUrl`, `description`, `actionType`, `actionIcon` to `DashboardWidgetSchema` ([#713](https://github.com/objectstack-ai/spec/issues/713))
404+
- [x] Add `colorVariant`, `actionUrl`, `description`, `actionType`, `actionIcon` to `DashboardWidgetSchema` ([#713](https://github.com/objectstack-ai/spec/issues/713))
405405
- [ ] Enhance `globalFilters` with `options`, `optionsFrom`, `defaultValue`, `scope`, `targetWidgets` ([#712](https://github.com/objectstack-ai/spec/issues/712))
406406
- [ ] Add `header` configuration to `DashboardSchema` with `showTitle`, `showDescription`, `actions` ([#714](https://github.com/objectstack-ai/spec/issues/714))
407407
- [ ] Add `pivotConfig` and `measures` array to `DashboardWidgetSchema` for multi-measure pivots ([#714](https://github.com/objectstack-ai/spec/issues/714))

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

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
DashboardSchema,
44
DashboardWidgetSchema,
55
Dashboard,
6+
WidgetColorVariantSchema,
7+
WidgetActionTypeSchema,
68
type Dashboard as DashboardType,
79
type DashboardWidget,
810
} from './dashboard.zod';
@@ -610,3 +612,243 @@ describe('DashboardSchema - dateRange', () => {
610612
expect(result.dateRange).toBeUndefined();
611613
});
612614
});
615+
616+
// ============================================================================
617+
// Protocol Enhancement Tests: colorVariant, description, actionUrl/actionType
618+
// ============================================================================
619+
620+
describe('WidgetColorVariantSchema', () => {
621+
it('should accept all color variants', () => {
622+
const variants = ['default', 'blue', 'teal', 'orange', 'purple', 'success', 'warning', 'danger'];
623+
variants.forEach(variant => {
624+
expect(() => WidgetColorVariantSchema.parse(variant)).not.toThrow();
625+
});
626+
});
627+
628+
it('should reject invalid color variants', () => {
629+
expect(() => WidgetColorVariantSchema.parse('red')).toThrow();
630+
expect(() => WidgetColorVariantSchema.parse('unknown')).toThrow();
631+
});
632+
});
633+
634+
describe('WidgetActionTypeSchema', () => {
635+
it('should accept all action types', () => {
636+
const types = ['url', 'modal', 'flow'];
637+
types.forEach(type => {
638+
expect(() => WidgetActionTypeSchema.parse(type)).not.toThrow();
639+
});
640+
});
641+
642+
it('should reject invalid action types', () => {
643+
expect(() => WidgetActionTypeSchema.parse('script')).toThrow();
644+
expect(() => WidgetActionTypeSchema.parse('invalid')).toThrow();
645+
});
646+
});
647+
648+
describe('DashboardWidgetSchema - colorVariant', () => {
649+
it('should accept widget with colorVariant', () => {
650+
const widget: DashboardWidget = {
651+
title: 'Total Revenue',
652+
type: 'metric',
653+
colorVariant: 'teal',
654+
layout: { x: 0, y: 0, w: 3, h: 2 },
655+
};
656+
const result = DashboardWidgetSchema.parse(widget);
657+
expect(result.colorVariant).toBe('teal');
658+
});
659+
660+
it('should accept widget without colorVariant (optional)', () => {
661+
const result = DashboardWidgetSchema.parse({
662+
type: 'metric',
663+
layout: { x: 0, y: 0, w: 3, h: 2 },
664+
});
665+
expect(result.colorVariant).toBeUndefined();
666+
});
667+
668+
it('should reject invalid colorVariant', () => {
669+
expect(() => DashboardWidgetSchema.parse({
670+
type: 'metric',
671+
colorVariant: 'neon',
672+
layout: { x: 0, y: 0, w: 3, h: 2 },
673+
})).toThrow();
674+
});
675+
});
676+
677+
describe('DashboardWidgetSchema - description', () => {
678+
it('should accept widget with string description', () => {
679+
const result = DashboardWidgetSchema.parse({
680+
title: 'Revenue',
681+
description: 'Year-to-date total revenue',
682+
type: 'metric',
683+
layout: { x: 0, y: 0, w: 3, h: 2 },
684+
});
685+
expect(result.description).toBe('Year-to-date total revenue');
686+
});
687+
688+
it('should accept widget with i18n description', () => {
689+
const result = DashboardWidgetSchema.parse({
690+
title: 'Revenue',
691+
description: { key: 'widgets.revenue.desc', defaultValue: 'Total revenue' },
692+
type: 'metric',
693+
layout: { x: 0, y: 0, w: 3, h: 2 },
694+
});
695+
expect(result.description).toEqual({ key: 'widgets.revenue.desc', defaultValue: 'Total revenue' });
696+
});
697+
698+
it('should accept widget without description (optional)', () => {
699+
const result = DashboardWidgetSchema.parse({
700+
type: 'metric',
701+
layout: { x: 0, y: 0, w: 3, h: 2 },
702+
});
703+
expect(result.description).toBeUndefined();
704+
});
705+
});
706+
707+
describe('DashboardWidgetSchema - actionUrl/actionType/actionIcon', () => {
708+
it('should accept widget with actionUrl and actionType', () => {
709+
const result = DashboardWidgetSchema.parse({
710+
title: 'Open Tickets',
711+
type: 'metric',
712+
actionUrl: 'https://example.com/tickets',
713+
actionType: 'url',
714+
layout: { x: 0, y: 0, w: 3, h: 2 },
715+
});
716+
expect(result.actionUrl).toBe('https://example.com/tickets');
717+
expect(result.actionType).toBe('url');
718+
});
719+
720+
it('should accept widget with actionIcon', () => {
721+
const result = DashboardWidgetSchema.parse({
722+
title: 'Details',
723+
type: 'metric',
724+
actionUrl: '/details',
725+
actionType: 'url',
726+
actionIcon: 'external-link',
727+
layout: { x: 0, y: 0, w: 3, h: 2 },
728+
});
729+
expect(result.actionIcon).toBe('external-link');
730+
});
731+
732+
it('should accept widget with modal action type', () => {
733+
const result = DashboardWidgetSchema.parse({
734+
title: 'Breakdown',
735+
type: 'metric',
736+
actionUrl: 'revenue_breakdown',
737+
actionType: 'modal',
738+
layout: { x: 0, y: 0, w: 3, h: 2 },
739+
});
740+
expect(result.actionType).toBe('modal');
741+
});
742+
743+
it('should accept widget with flow action type', () => {
744+
const result = DashboardWidgetSchema.parse({
745+
title: 'Refresh Data',
746+
type: 'metric',
747+
actionUrl: 'refresh_pipeline_flow',
748+
actionType: 'flow',
749+
layout: { x: 0, y: 0, w: 3, h: 2 },
750+
});
751+
expect(result.actionType).toBe('flow');
752+
});
753+
754+
it('should accept widget without action fields (optional)', () => {
755+
const result = DashboardWidgetSchema.parse({
756+
type: 'metric',
757+
layout: { x: 0, y: 0, w: 3, h: 2 },
758+
});
759+
expect(result.actionUrl).toBeUndefined();
760+
expect(result.actionType).toBeUndefined();
761+
expect(result.actionIcon).toBeUndefined();
762+
});
763+
764+
it('should reject invalid actionType', () => {
765+
expect(() => DashboardWidgetSchema.parse({
766+
type: 'metric',
767+
actionType: 'invalid',
768+
layout: { x: 0, y: 0, w: 3, h: 2 },
769+
})).toThrow();
770+
});
771+
});
772+
773+
describe('DashboardWidgetSchema - combined new fields', () => {
774+
it('should accept KPI widget with all new fields', () => {
775+
const widget: DashboardWidget = {
776+
title: 'Revenue',
777+
description: 'Q4 total revenue across all regions',
778+
type: 'metric',
779+
colorVariant: 'success',
780+
actionUrl: 'https://reports.example.com/revenue',
781+
actionType: 'url',
782+
actionIcon: 'external-link',
783+
object: 'opportunity',
784+
valueField: 'amount',
785+
aggregate: 'sum',
786+
layout: { x: 0, y: 0, w: 3, h: 2 },
787+
};
788+
789+
const result = DashboardWidgetSchema.parse(widget);
790+
expect(result.description).toBe('Q4 total revenue across all regions');
791+
expect(result.colorVariant).toBe('success');
792+
expect(result.actionUrl).toBe('https://reports.example.com/revenue');
793+
expect(result.actionType).toBe('url');
794+
expect(result.actionIcon).toBe('external-link');
795+
});
796+
797+
it('should work in a full dashboard with color-coded KPI cards', () => {
798+
const dashboard = Dashboard.create({
799+
name: 'kpi_dashboard',
800+
label: 'KPI Dashboard',
801+
widgets: [
802+
{
803+
title: 'Revenue',
804+
description: 'Total quarterly revenue',
805+
type: 'metric',
806+
colorVariant: 'success',
807+
object: 'opportunity',
808+
valueField: 'amount',
809+
aggregate: 'sum',
810+
layout: { x: 0, y: 0, w: 3, h: 2 },
811+
},
812+
{
813+
title: 'Open Issues',
814+
description: 'Unresolved support tickets',
815+
type: 'metric',
816+
colorVariant: 'warning',
817+
actionUrl: '/issues',
818+
actionType: 'url',
819+
object: 'case',
820+
aggregate: 'count',
821+
layout: { x: 3, y: 0, w: 3, h: 2 },
822+
},
823+
{
824+
title: 'Critical Bugs',
825+
description: 'P0/P1 bugs requiring attention',
826+
type: 'metric',
827+
colorVariant: 'danger',
828+
actionUrl: 'bug_triage_flow',
829+
actionType: 'flow',
830+
actionIcon: 'alert-triangle',
831+
object: 'bug',
832+
aggregate: 'count',
833+
layout: { x: 6, y: 0, w: 3, h: 2 },
834+
},
835+
{
836+
title: 'Team Velocity',
837+
type: 'bar',
838+
colorVariant: 'blue',
839+
object: 'sprint',
840+
categoryField: 'sprint_name',
841+
valueField: 'story_points',
842+
aggregate: 'sum',
843+
layout: { x: 0, y: 2, w: 12, h: 4 },
844+
},
845+
],
846+
});
847+
848+
expect(dashboard.widgets).toHaveLength(4);
849+
expect(dashboard.widgets[0].colorVariant).toBe('success');
850+
expect(dashboard.widgets[1].actionUrl).toBe('/issues');
851+
expect(dashboard.widgets[2].description).toBe('P0/P1 bugs requiring attention');
852+
expect(dashboard.widgets[3].colorVariant).toBe('blue');
853+
});
854+
});

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,57 @@ import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
77
import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod';
88
import { ResponsiveConfigSchema, PerformanceConfigSchema } from './responsive.zod';
99

10+
/**
11+
* Color variant for dashboard widgets (e.g., KPI cards).
12+
*/
13+
export const WidgetColorVariantSchema = z.enum([
14+
'default',
15+
'blue',
16+
'teal',
17+
'orange',
18+
'purple',
19+
'success',
20+
'warning',
21+
'danger',
22+
]).describe('Widget color variant');
23+
24+
/**
25+
* Action type for widget action buttons.
26+
*/
27+
export const WidgetActionTypeSchema = z.enum([
28+
'url',
29+
'modal',
30+
'flow',
31+
]).describe('Widget action type');
32+
1033
/**
1134
* Dashboard Widget Schema
1235
* A single component on the dashboard grid.
1336
*/
1437
export const DashboardWidgetSchema = z.object({
1538
/** Widget Title */
1639
title: I18nLabelSchema.optional().describe('Widget title'),
40+
41+
/** Widget Description (displayed below the title) */
42+
description: I18nLabelSchema.optional().describe('Widget description text below the header'),
1743

1844
/** Visualization Type */
1945
type: ChartTypeSchema.default('metric').describe('Visualization type'),
2046

2147
/** Chart Configuration */
2248
chartConfig: ChartConfigSchema.optional().describe('Chart visualization configuration'),
49+
50+
/** Color variant for the widget (e.g., KPI card accent color) */
51+
colorVariant: WidgetColorVariantSchema.optional().describe('Widget color variant for theming'),
52+
53+
/** Action URL for the widget header action button */
54+
actionUrl: z.string().optional().describe('URL or target for the widget action button'),
55+
56+
/** Action type for the widget header action button */
57+
actionType: WidgetActionTypeSchema.optional().describe('Type of action for the widget action button'),
58+
59+
/** Icon for the widget header action button */
60+
actionIcon: z.string().optional().describe('Icon identifier for the widget action button'),
2361

2462
/** Data Source Object */
2563
object: z.string().optional().describe('Data source object name'),
@@ -129,6 +167,8 @@ export const DashboardSchema = z.object({
129167
export type Dashboard = z.infer<typeof DashboardSchema>;
130168
export type DashboardInput = z.input<typeof DashboardSchema>;
131169
export type DashboardWidget = z.infer<typeof DashboardWidgetSchema>;
170+
export type WidgetColorVariant = z.infer<typeof WidgetColorVariantSchema>;
171+
export type WidgetActionType = z.infer<typeof WidgetActionTypeSchema>;
132172

133173
/**
134174
* Dashboard Factory Helper

0 commit comments

Comments
 (0)