From fb07f57081cd4958ba3f63cdeb504363e75eddff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 06:06:58 +0000 Subject: [PATCH 1/2] Initial plan From b14967b8d2d538ec887fa177e149bc83c8292de2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 06:11:29 +0000 Subject: [PATCH 2/2] feat: add colorVariant, description, actionUrl/actionType/actionIcon to DashboardWidgetSchema Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 2 +- packages/spec/src/ui/dashboard.test.ts | 242 +++++++++++++++++++++++++ packages/spec/src/ui/dashboard.zod.ts | 40 ++++ 3 files changed, 283 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 363dd0bc85..4f357c6a03 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -401,7 +401,7 @@ The following renames are planned for packages that implement core service contr Protocol enhancements and core component implementations for dashboard feature parity. **Spec Protocol Changes:** -- [ ] Add `colorVariant`, `actionUrl`, `description`, `actionType`, `actionIcon` to `DashboardWidgetSchema` ([#713](https://github.com/objectstack-ai/spec/issues/713)) +- [x] Add `colorVariant`, `actionUrl`, `description`, `actionType`, `actionIcon` to `DashboardWidgetSchema` ([#713](https://github.com/objectstack-ai/spec/issues/713)) - [ ] Enhance `globalFilters` with `options`, `optionsFrom`, `defaultValue`, `scope`, `targetWidgets` ([#712](https://github.com/objectstack-ai/spec/issues/712)) - [ ] Add `header` configuration to `DashboardSchema` with `showTitle`, `showDescription`, `actions` ([#714](https://github.com/objectstack-ai/spec/issues/714)) - [ ] Add `pivotConfig` and `measures` array to `DashboardWidgetSchema` for multi-measure pivots ([#714](https://github.com/objectstack-ai/spec/issues/714)) diff --git a/packages/spec/src/ui/dashboard.test.ts b/packages/spec/src/ui/dashboard.test.ts index ebe8adb3a3..4f04edc1ed 100644 --- a/packages/spec/src/ui/dashboard.test.ts +++ b/packages/spec/src/ui/dashboard.test.ts @@ -3,6 +3,8 @@ import { DashboardSchema, DashboardWidgetSchema, Dashboard, + WidgetColorVariantSchema, + WidgetActionTypeSchema, type Dashboard as DashboardType, type DashboardWidget, } from './dashboard.zod'; @@ -610,3 +612,243 @@ describe('DashboardSchema - dateRange', () => { expect(result.dateRange).toBeUndefined(); }); }); + +// ============================================================================ +// Protocol Enhancement Tests: colorVariant, description, actionUrl/actionType +// ============================================================================ + +describe('WidgetColorVariantSchema', () => { + it('should accept all color variants', () => { + const variants = ['default', 'blue', 'teal', 'orange', 'purple', 'success', 'warning', 'danger']; + variants.forEach(variant => { + expect(() => WidgetColorVariantSchema.parse(variant)).not.toThrow(); + }); + }); + + it('should reject invalid color variants', () => { + expect(() => WidgetColorVariantSchema.parse('red')).toThrow(); + expect(() => WidgetColorVariantSchema.parse('unknown')).toThrow(); + }); +}); + +describe('WidgetActionTypeSchema', () => { + it('should accept all action types', () => { + const types = ['url', 'modal', 'flow']; + types.forEach(type => { + expect(() => WidgetActionTypeSchema.parse(type)).not.toThrow(); + }); + }); + + it('should reject invalid action types', () => { + expect(() => WidgetActionTypeSchema.parse('script')).toThrow(); + expect(() => WidgetActionTypeSchema.parse('invalid')).toThrow(); + }); +}); + +describe('DashboardWidgetSchema - colorVariant', () => { + it('should accept widget with colorVariant', () => { + const widget: DashboardWidget = { + title: 'Total Revenue', + type: 'metric', + colorVariant: 'teal', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }; + const result = DashboardWidgetSchema.parse(widget); + expect(result.colorVariant).toBe('teal'); + }); + + it('should accept widget without colorVariant (optional)', () => { + const result = DashboardWidgetSchema.parse({ + type: 'metric', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.colorVariant).toBeUndefined(); + }); + + it('should reject invalid colorVariant', () => { + expect(() => DashboardWidgetSchema.parse({ + type: 'metric', + colorVariant: 'neon', + layout: { x: 0, y: 0, w: 3, h: 2 }, + })).toThrow(); + }); +}); + +describe('DashboardWidgetSchema - description', () => { + it('should accept widget with string description', () => { + const result = DashboardWidgetSchema.parse({ + title: 'Revenue', + description: 'Year-to-date total revenue', + type: 'metric', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.description).toBe('Year-to-date total revenue'); + }); + + it('should accept widget with i18n description', () => { + const result = DashboardWidgetSchema.parse({ + title: 'Revenue', + description: { key: 'widgets.revenue.desc', defaultValue: 'Total revenue' }, + type: 'metric', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.description).toEqual({ key: 'widgets.revenue.desc', defaultValue: 'Total revenue' }); + }); + + it('should accept widget without description (optional)', () => { + const result = DashboardWidgetSchema.parse({ + type: 'metric', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.description).toBeUndefined(); + }); +}); + +describe('DashboardWidgetSchema - actionUrl/actionType/actionIcon', () => { + it('should accept widget with actionUrl and actionType', () => { + const result = DashboardWidgetSchema.parse({ + title: 'Open Tickets', + type: 'metric', + actionUrl: 'https://example.com/tickets', + actionType: 'url', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.actionUrl).toBe('https://example.com/tickets'); + expect(result.actionType).toBe('url'); + }); + + it('should accept widget with actionIcon', () => { + const result = DashboardWidgetSchema.parse({ + title: 'Details', + type: 'metric', + actionUrl: '/details', + actionType: 'url', + actionIcon: 'external-link', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.actionIcon).toBe('external-link'); + }); + + it('should accept widget with modal action type', () => { + const result = DashboardWidgetSchema.parse({ + title: 'Breakdown', + type: 'metric', + actionUrl: 'revenue_breakdown', + actionType: 'modal', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.actionType).toBe('modal'); + }); + + it('should accept widget with flow action type', () => { + const result = DashboardWidgetSchema.parse({ + title: 'Refresh Data', + type: 'metric', + actionUrl: 'refresh_pipeline_flow', + actionType: 'flow', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.actionType).toBe('flow'); + }); + + it('should accept widget without action fields (optional)', () => { + const result = DashboardWidgetSchema.parse({ + type: 'metric', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }); + expect(result.actionUrl).toBeUndefined(); + expect(result.actionType).toBeUndefined(); + expect(result.actionIcon).toBeUndefined(); + }); + + it('should reject invalid actionType', () => { + expect(() => DashboardWidgetSchema.parse({ + type: 'metric', + actionType: 'invalid', + layout: { x: 0, y: 0, w: 3, h: 2 }, + })).toThrow(); + }); +}); + +describe('DashboardWidgetSchema - combined new fields', () => { + it('should accept KPI widget with all new fields', () => { + const widget: DashboardWidget = { + title: 'Revenue', + description: 'Q4 total revenue across all regions', + type: 'metric', + colorVariant: 'success', + actionUrl: 'https://reports.example.com/revenue', + actionType: 'url', + actionIcon: 'external-link', + object: 'opportunity', + valueField: 'amount', + aggregate: 'sum', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }; + + const result = DashboardWidgetSchema.parse(widget); + expect(result.description).toBe('Q4 total revenue across all regions'); + expect(result.colorVariant).toBe('success'); + expect(result.actionUrl).toBe('https://reports.example.com/revenue'); + expect(result.actionType).toBe('url'); + expect(result.actionIcon).toBe('external-link'); + }); + + it('should work in a full dashboard with color-coded KPI cards', () => { + const dashboard = Dashboard.create({ + name: 'kpi_dashboard', + label: 'KPI Dashboard', + widgets: [ + { + title: 'Revenue', + description: 'Total quarterly revenue', + type: 'metric', + colorVariant: 'success', + object: 'opportunity', + valueField: 'amount', + aggregate: 'sum', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }, + { + title: 'Open Issues', + description: 'Unresolved support tickets', + type: 'metric', + colorVariant: 'warning', + actionUrl: '/issues', + actionType: 'url', + object: 'case', + aggregate: 'count', + layout: { x: 3, y: 0, w: 3, h: 2 }, + }, + { + title: 'Critical Bugs', + description: 'P0/P1 bugs requiring attention', + type: 'metric', + colorVariant: 'danger', + actionUrl: 'bug_triage_flow', + actionType: 'flow', + actionIcon: 'alert-triangle', + object: 'bug', + aggregate: 'count', + layout: { x: 6, y: 0, w: 3, h: 2 }, + }, + { + title: 'Team Velocity', + type: 'bar', + colorVariant: 'blue', + object: 'sprint', + categoryField: 'sprint_name', + valueField: 'story_points', + aggregate: 'sum', + layout: { x: 0, y: 2, w: 12, h: 4 }, + }, + ], + }); + + expect(dashboard.widgets).toHaveLength(4); + expect(dashboard.widgets[0].colorVariant).toBe('success'); + expect(dashboard.widgets[1].actionUrl).toBe('/issues'); + expect(dashboard.widgets[2].description).toBe('P0/P1 bugs requiring attention'); + expect(dashboard.widgets[3].colorVariant).toBe('blue'); + }); +}); diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 88848f1f38..689d564e85 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -7,6 +7,29 @@ import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; import { ResponsiveConfigSchema, PerformanceConfigSchema } from './responsive.zod'; +/** + * Color variant for dashboard widgets (e.g., KPI cards). + */ +export const WidgetColorVariantSchema = z.enum([ + 'default', + 'blue', + 'teal', + 'orange', + 'purple', + 'success', + 'warning', + 'danger', +]).describe('Widget color variant'); + +/** + * Action type for widget action buttons. + */ +export const WidgetActionTypeSchema = z.enum([ + 'url', + 'modal', + 'flow', +]).describe('Widget action type'); + /** * Dashboard Widget Schema * A single component on the dashboard grid. @@ -14,12 +37,27 @@ import { ResponsiveConfigSchema, PerformanceConfigSchema } from './responsive.zo export const DashboardWidgetSchema = z.object({ /** Widget Title */ title: I18nLabelSchema.optional().describe('Widget title'), + + /** Widget Description (displayed below the title) */ + description: I18nLabelSchema.optional().describe('Widget description text below the header'), /** Visualization Type */ type: ChartTypeSchema.default('metric').describe('Visualization type'), /** Chart Configuration */ chartConfig: ChartConfigSchema.optional().describe('Chart visualization configuration'), + + /** Color variant for the widget (e.g., KPI card accent color) */ + colorVariant: WidgetColorVariantSchema.optional().describe('Widget color variant for theming'), + + /** Action URL for the widget header action button */ + actionUrl: z.string().optional().describe('URL or target for the widget action button'), + + /** Action type for the widget header action button */ + actionType: WidgetActionTypeSchema.optional().describe('Type of action for the widget action button'), + + /** Icon for the widget header action button */ + actionIcon: z.string().optional().describe('Icon identifier for the widget action button'), /** Data Source Object */ object: z.string().optional().describe('Data source object name'), @@ -129,6 +167,8 @@ export const DashboardSchema = z.object({ export type Dashboard = z.infer; export type DashboardInput = z.input; export type DashboardWidget = z.infer; +export type WidgetColorVariant = z.infer; +export type WidgetActionType = z.infer; /** * Dashboard Factory Helper