diff --git a/ROADMAP.md b/ROADMAP.md index 2597e5efb..60299efe3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -846,6 +846,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th - [x] **P2: App Branding** — `logo`, `favicon`, `backgroundColor` fields on CRM app - [x] **P3: Pages** — Settings page (utility) and Getting Started page (onboarding) - [x] **P2: Spec Compliance Audit** — Fixed `variant: 'danger'` → `'destructive'` (4 actions), `columns: string` → `number` (33 form sections), added `type: 'dashboard'` to dashboard +- [x] **P2: Dashboard Widget Spec Alignment** — Added `id`, `title`, `object`, `categoryField`, `valueField`, `aggregate` to all dashboard widgets across CRM, Todo, and Kitchen Sink examples (5 new spec-compliance tests) - [x] **P2: i18n (10 locales)** — Full CRM metadata translations for en, zh, ja, ko, de, fr, es, pt, ru, ar — objects, fields, fieldOptions, navigation, actions, views, formSections, dashboard, reports, pages (24 tests) ### Ecosystem & Marketplace diff --git a/examples/crm/src/__tests__/crm-metadata.test.ts b/examples/crm/src/__tests__/crm-metadata.test.ts index d9382cbc8..2bd7449c3 100644 --- a/examples/crm/src/__tests__/crm-metadata.test.ts +++ b/examples/crm/src/__tests__/crm-metadata.test.ts @@ -175,6 +175,47 @@ describe('CRM Metadata Spec Compliance', () => { expect(widget.layout).toHaveProperty('h'); } }); + + it('all widgets have unique id', () => { + const ids = CrmDashboard.widgets.map((w) => w.id); + for (const id of ids) { + expect(typeof id).toBe('string'); + expect(id!.length).toBeGreaterThan(0); + } + expect(new Set(ids).size).toBe(ids.length); + }); + + it('all widgets have title', () => { + for (const widget of CrmDashboard.widgets) { + expect(typeof widget.title).toBe('string'); + expect(widget.title!.length).toBeGreaterThan(0); + } + }); + + it('metric widgets have title matching options.label', () => { + const metrics = CrmDashboard.widgets.filter((w) => w.type === 'metric'); + for (const widget of metrics) { + const opts = widget.options as { label?: string }; + expect(widget.title).toBe(opts.label); + } + }); + + it('all widgets have object data binding', () => { + for (const widget of CrmDashboard.widgets) { + expect(typeof widget.object).toBe('string'); + expect(widget.object!.length).toBeGreaterThan(0); + } + }); + + it('chart widgets have categoryField, valueField, and aggregate', () => { + const chartTypes = ['bar', 'area', 'donut', 'line', 'pie']; + const charts = CrmDashboard.widgets.filter((w) => chartTypes.includes(w.type)); + for (const widget of charts) { + expect(typeof widget.categoryField).toBe('string'); + expect(typeof widget.valueField).toBe('string'); + expect(typeof widget.aggregate).toBe('string'); + } + }); }); describe('Reports', () => { diff --git a/examples/crm/src/dashboards/crm.dashboard.ts b/examples/crm/src/dashboards/crm.dashboard.ts index ee980c75f..573ed285f 100644 --- a/examples/crm/src/dashboards/crm.dashboard.ts +++ b/examples/crm/src/dashboards/crm.dashboard.ts @@ -5,7 +5,10 @@ export const CrmDashboard = { widgets: [ // --- KPI Row --- { + id: 'crm_total_revenue', + title: 'Total Revenue', type: 'metric' as const, + object: 'opportunity', layout: { x: 0, y: 0, w: 1, h: 1 }, options: { label: 'Total Revenue', @@ -15,7 +18,10 @@ export const CrmDashboard = { } }, { + id: 'crm_active_deals', + title: 'Active Deals', type: 'metric' as const, + object: 'opportunity', layout: { x: 1, y: 0, w: 1, h: 1 }, options: { label: 'Active Deals', @@ -25,7 +31,10 @@ export const CrmDashboard = { } }, { + id: 'crm_win_rate', + title: 'Win Rate', type: 'metric' as const, + object: 'opportunity', layout: { x: 2, y: 0, w: 1, h: 1 }, options: { label: 'Win Rate', @@ -35,7 +44,10 @@ export const CrmDashboard = { } }, { + id: 'crm_avg_deal_size', + title: 'Avg Deal Size', type: 'metric' as const, + object: 'opportunity', layout: { x: 3, y: 0, w: 1, h: 1 }, options: { label: 'Avg Deal Size', @@ -47,8 +59,13 @@ export const CrmDashboard = { // --- Row 2: Charts --- { + id: 'crm_revenue_trends', title: 'Revenue Trends', - type: 'area' as const, + type: 'area' as const, + object: 'opportunity', + categoryField: 'month', + valueField: 'revenue', + aggregate: 'sum', layout: { x: 0, y: 1, w: 3, h: 2 }, options: { xField: 'month', @@ -68,8 +85,13 @@ export const CrmDashboard = { }, }, { + id: 'crm_lead_source', title: 'Lead Source', type: 'donut' as const, + object: 'opportunity', + categoryField: 'source', + valueField: 'value', + aggregate: 'count', layout: { x: 3, y: 1, w: 1, h: 2 }, options: { xField: 'source', @@ -88,8 +110,13 @@ export const CrmDashboard = { // --- Row 3: More Charts --- { + id: 'crm_pipeline_by_stage', title: 'Pipeline by Stage', type: 'bar' as const, + object: 'opportunity', + categoryField: 'stage', + valueField: 'amount', + aggregate: 'sum', layout: { x: 0, y: 3, w: 2, h: 2 }, options: { xField: 'stage', @@ -107,8 +134,13 @@ export const CrmDashboard = { }, }, { + id: 'crm_top_products', title: 'Top Products', type: 'bar' as const, + object: 'product', + categoryField: 'name', + valueField: 'sales', + aggregate: 'sum', layout: { x: 2, y: 3, w: 2, h: 2 }, options: { xField: 'name', @@ -127,8 +159,10 @@ export const CrmDashboard = { // --- Row 4: Table --- { + id: 'crm_recent_opportunities', title: 'Recent Opportunities', type: 'table' as const, + object: 'opportunity', layout: { x: 0, y: 5, w: 4, h: 2 }, options: { columns: [ @@ -152,8 +186,13 @@ export const CrmDashboard = { // --- Row 5: Dynamic KPI from Object Data --- { + id: 'crm_revenue_by_account', title: 'Revenue by Account', type: 'bar' as const, + object: 'opportunity', + categoryField: 'account', + valueField: 'total', + aggregate: 'sum', layout: { x: 0, y: 7, w: 4, h: 2 }, options: { xField: 'account', diff --git a/examples/kitchen-sink/objectstack.config.ts b/examples/kitchen-sink/objectstack.config.ts index aa52f88a0..86a246d4f 100644 --- a/examples/kitchen-sink/objectstack.config.ts +++ b/examples/kitchen-sink/objectstack.config.ts @@ -73,7 +73,10 @@ export default defineStack({ widgets: [ // --- KPI Row --- { + id: 'ks_total_records', + title: 'Total Records', type: 'metric', + object: 'kitchen_sink', layout: { x: 0, y: 0, w: 1, h: 1 }, options: { label: 'Total Records', @@ -82,7 +85,10 @@ export default defineStack({ }, }, { + id: 'ks_active_items', + title: 'Active Items', type: 'metric', + object: 'kitchen_sink', layout: { x: 1, y: 0, w: 1, h: 1 }, options: { label: 'Active Items', @@ -92,7 +98,10 @@ export default defineStack({ }, }, { + id: 'ks_total_value', + title: 'Total Value', type: 'metric', + object: 'kitchen_sink', layout: { x: 2, y: 0, w: 1, h: 1 }, options: { label: 'Total Value', @@ -102,7 +111,10 @@ export default defineStack({ }, }, { + id: 'ks_avg_rating', + title: 'Avg Rating', type: 'metric', + object: 'kitchen_sink', layout: { x: 3, y: 0, w: 1, h: 1 }, options: { label: 'Avg Rating', @@ -114,8 +126,13 @@ export default defineStack({ // --- Charts Row --- { + id: 'ks_records_by_category', title: 'Records by Category', type: 'donut', + object: 'kitchen_sink', + categoryField: 'category', + valueField: 'count', + aggregate: 'count', layout: { x: 0, y: 1, w: 2, h: 2 }, options: { xField: 'category', @@ -131,8 +148,13 @@ export default defineStack({ }, }, { + id: 'ks_value_distribution', title: 'Value Distribution', type: 'bar', + object: 'kitchen_sink', + categoryField: 'name', + valueField: 'amount', + aggregate: 'sum', layout: { x: 2, y: 1, w: 2, h: 2 }, options: { xField: 'name', @@ -152,8 +174,13 @@ export default defineStack({ // --- Trend Row --- { + id: 'ks_monthly_trend', title: 'Monthly Trend', type: 'area', + object: 'kitchen_sink', + categoryField: 'month', + valueField: 'value', + aggregate: 'sum', layout: { x: 0, y: 3, w: 4, h: 2 }, options: { xField: 'month', diff --git a/examples/todo/objectstack.config.ts b/examples/todo/objectstack.config.ts index 8b343831d..b0bbcb672 100644 --- a/examples/todo/objectstack.config.ts +++ b/examples/todo/objectstack.config.ts @@ -111,7 +111,10 @@ export default defineStack({ label: 'Task Overview', widgets: [ { + id: 'todo_total_tasks', + title: 'Total Tasks', type: 'metric', + object: 'todo_task', layout: { x: 0, y: 0, w: 1, h: 1 }, options: { label: 'Total Tasks', @@ -120,7 +123,10 @@ export default defineStack({ }, }, { + id: 'todo_in_progress', + title: 'In Progress', type: 'metric', + object: 'todo_task', layout: { x: 1, y: 0, w: 1, h: 1 }, options: { label: 'In Progress', @@ -130,7 +136,10 @@ export default defineStack({ }, }, { + id: 'todo_completed', + title: 'Completed', type: 'metric', + object: 'todo_task', layout: { x: 2, y: 0, w: 1, h: 1 }, options: { label: 'Completed', @@ -140,7 +149,10 @@ export default defineStack({ }, }, { + id: 'todo_overdue', + title: 'Overdue', type: 'metric', + object: 'todo_task', layout: { x: 3, y: 0, w: 1, h: 1 }, options: { label: 'Overdue', @@ -150,8 +162,13 @@ export default defineStack({ }, }, { + id: 'todo_tasks_by_status', title: 'Tasks by Status', type: 'donut', + object: 'todo_task', + categoryField: 'status', + valueField: 'count', + aggregate: 'count', layout: { x: 0, y: 1, w: 2, h: 2 }, options: { xField: 'status', @@ -169,8 +186,13 @@ export default defineStack({ }, }, { + id: 'todo_tasks_by_category', title: 'Tasks by Category', type: 'bar', + object: 'todo_task', + categoryField: 'category', + valueField: 'count', + aggregate: 'count', layout: { x: 2, y: 1, w: 2, h: 2 }, options: { xField: 'category',