Skip to content

Commit b313e6c

Browse files
authored
Merge pull request #838 from objectstack-ai/copilot/fix-crm-dashboard-blank-issue
2 parents c197734 + 3ec060b commit b313e6c

File tree

14 files changed

+345
-63
lines changed

14 files changed

+345
-63
lines changed

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
863863
- [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)
864864
- [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)
865865
- [x] **P2: Full Examples Metadata Audit** — Systematic spec compliance audit across all 4 examples: added `type: 'dashboard'` + `description` to todo/kitchen-sink dashboards, refactored msw-todo to use `ObjectSchema.create` + `Field.*` with snake_case field names, added explicit views to kitchen-sink and msw-todo, added missing `successMessage` on CRM opportunity action, 21 automated compliance tests
866+
- [x] **P2: CRM Dashboard Full provider:'object' Adaptation** — Converted all chart and table widgets in CRM dashboard from static `provider: 'value'` to dynamic `provider: 'object'` with aggregation configs. 12 widgets total: 4 KPI metrics (static), 7 charts (sum/count/avg/max aggregation across opportunity, product, order objects), 1 table (dynamic fetch). Cross-object coverage (order), diverse aggregate functions (sum, count, avg, max). Fixed table `close_date` field alignment. Added i18n for 2 new widgets (10 locales). 9 new CRM metadata tests, 6 new DashboardRenderer rendering tests (area/donut/line/cross-object + edge cases). All provider:'object' paths covered.
866867

867868
### Ecosystem & Marketplace
868869
- Plugin marketplace website with search, ratings, and install count

examples/crm/src/__tests__/crm-metadata.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,83 @@ describe('CRM Metadata Spec Compliance', () => {
212212
expect(typeof widget.aggregate).toBe('string');
213213
}
214214
});
215+
216+
it('all chart widgets use provider: object for dynamic data', () => {
217+
const chartTypes = ['bar', 'area', 'donut', 'line', 'pie'];
218+
const charts = CrmDashboard.widgets.filter((w) => chartTypes.includes(w.type));
219+
expect(charts.length).toBeGreaterThanOrEqual(5);
220+
for (const widget of charts) {
221+
const data = (widget.options as any)?.data;
222+
expect(data).toBeDefined();
223+
expect(data.provider).toBe('object');
224+
expect(typeof data.object).toBe('string');
225+
expect(data.object.length).toBeGreaterThan(0);
226+
}
227+
});
228+
229+
it('all chart widgets with provider: object have valid aggregate config', () => {
230+
const chartTypes = ['bar', 'area', 'donut', 'line', 'pie'];
231+
const charts = CrmDashboard.widgets.filter((w) => chartTypes.includes(w.type));
232+
for (const widget of charts) {
233+
const data = (widget.options as any)?.data;
234+
expect(data.aggregate).toBeDefined();
235+
expect(typeof data.aggregate.field).toBe('string');
236+
expect(typeof data.aggregate.function).toBe('string');
237+
expect(typeof data.aggregate.groupBy).toBe('string');
238+
expect(['sum', 'count', 'avg', 'min', 'max']).toContain(data.aggregate.function);
239+
}
240+
});
241+
242+
it('table widget uses provider: object for dynamic data', () => {
243+
const tables = CrmDashboard.widgets.filter((w) => w.type === 'table');
244+
expect(tables.length).toBeGreaterThanOrEqual(1);
245+
for (const widget of tables) {
246+
const data = (widget.options as any)?.data;
247+
expect(data).toBeDefined();
248+
expect(data.provider).toBe('object');
249+
expect(typeof data.object).toBe('string');
250+
}
251+
});
252+
253+
it('aggregate groupBy fields align with widget categoryField', () => {
254+
const chartTypes = ['bar', 'area', 'donut', 'line', 'pie'];
255+
const charts = CrmDashboard.widgets.filter((w) => chartTypes.includes(w.type));
256+
for (const widget of charts) {
257+
const data = (widget.options as any)?.data;
258+
if (data?.aggregate?.groupBy) {
259+
expect(data.aggregate.groupBy).toBe(widget.categoryField);
260+
}
261+
}
262+
});
263+
264+
it('aggregate field names align with widget valueField', () => {
265+
const chartTypes = ['bar', 'area', 'donut', 'line', 'pie'];
266+
const charts = CrmDashboard.widgets.filter((w) => chartTypes.includes(w.type));
267+
for (const widget of charts) {
268+
const data = (widget.options as any)?.data;
269+
if (data?.aggregate?.field) {
270+
expect(data.aggregate.field).toBe(widget.valueField);
271+
}
272+
}
273+
});
274+
275+
it('dashboard covers diverse aggregate functions (sum, count, avg, max)', () => {
276+
const chartTypes = ['bar', 'area', 'donut', 'line', 'pie'];
277+
const charts = CrmDashboard.widgets.filter((w) => chartTypes.includes(w.type));
278+
const aggFns = new Set(charts.map((w) => (w.options as any)?.data?.aggregate?.function));
279+
expect(aggFns.has('sum')).toBe(true);
280+
expect(aggFns.has('count')).toBe(true);
281+
expect(aggFns.has('avg')).toBe(true);
282+
expect(aggFns.has('max')).toBe(true);
283+
});
284+
285+
it('dashboard includes cross-object widgets (order)', () => {
286+
const orderWidgets = CrmDashboard.widgets.filter((w) => w.object === 'order');
287+
expect(orderWidgets.length).toBeGreaterThanOrEqual(1);
288+
const data = (orderWidgets[0].options as any)?.data;
289+
expect(data.provider).toBe('object');
290+
expect(data.object).toBe('order');
291+
});
215292
});
216293

217294
describe('Reports', () => {
@@ -383,6 +460,8 @@ describe('CRM i18n Completeness', () => {
383460
expect(enLocale.dashboard.widgets.topProducts).toBeDefined();
384461
expect(enLocale.dashboard.widgets.recentOpportunities).toBeDefined();
385462
expect(enLocale.dashboard.widgets.revenueByAccount).toBeDefined();
463+
expect(enLocale.dashboard.widgets.avgDealSizeByStage).toBeDefined();
464+
expect(enLocale.dashboard.widgets.ordersByStatus).toBeDefined();
386465
});
387466
});
388467
});

examples/crm/src/dashboards/crm.dashboard.ts

Lines changed: 68 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -53,56 +53,45 @@ export const CrmDashboard = {
5353
}
5454
},
5555

56-
// --- Row 2: Charts ---
56+
// --- Row 2: Charts (provider: 'object' — dynamic aggregation) ---
5757
{
5858
title: 'Revenue Trends',
5959
type: 'area' as const,
6060
object: 'opportunity',
61-
categoryField: 'month',
62-
valueField: 'revenue',
61+
categoryField: 'stage',
62+
valueField: 'expected_revenue',
6363
aggregate: 'sum',
6464
layout: { x: 0, y: 1, w: 3, h: 2 },
6565
options: {
66-
xField: 'month',
67-
yField: 'revenue',
66+
xField: 'stage',
67+
yField: 'expected_revenue',
6868
data: {
69-
provider: 'value' as const,
70-
items: [
71-
{ month: 'Jan', revenue: 155000 },
72-
{ month: 'Feb', revenue: 87000 },
73-
{ month: 'Mar', revenue: 48000 },
74-
{ month: 'Apr', revenue: 61000 },
75-
{ month: 'May', revenue: 55000 },
76-
{ month: 'Jun', revenue: 67000 },
77-
{ month: 'Jul', revenue: 72000 }
78-
]
69+
provider: 'object' as const,
70+
object: 'opportunity',
71+
aggregate: { field: 'expected_revenue', function: 'sum' as const, groupBy: 'stage' }
7972
}
8073
},
8174
},
8275
{
8376
title: 'Lead Source',
8477
type: 'donut' as const,
8578
object: 'opportunity',
86-
categoryField: 'source',
87-
valueField: 'value',
79+
categoryField: 'lead_source',
80+
valueField: 'count',
8881
aggregate: 'count',
8982
layout: { x: 3, y: 1, w: 1, h: 2 },
9083
options: {
91-
xField: 'source',
92-
yField: 'value',
84+
xField: 'lead_source',
85+
yField: 'count',
9386
data: {
94-
provider: 'value' as const,
95-
items: [
96-
{ source: 'Web', value: 2 },
97-
{ source: 'Referral', value: 1 },
98-
{ source: 'Partner', value: 1 },
99-
{ source: 'Existing Business', value: 3 }
100-
]
87+
provider: 'object' as const,
88+
object: 'opportunity',
89+
aggregate: { field: 'count', function: 'count' as const, groupBy: 'lead_source' }
10190
}
10291
},
10392
},
10493

105-
// --- Row 3: More Charts ---
94+
// --- Row 3: More Charts (provider: 'object' — dynamic aggregation) ---
10695
{
10796
title: 'Pipeline by Stage',
10897
type: 'bar' as const,
@@ -115,41 +104,32 @@ export const CrmDashboard = {
115104
xField: 'stage',
116105
yField: 'amount',
117106
data: {
118-
provider: 'value' as const,
119-
items: [
120-
{ stage: 'Prospecting', amount: 250000 },
121-
{ stage: 'Qualification', amount: 35000 },
122-
{ stage: 'Proposal', amount: 85000 },
123-
{ stage: 'Negotiation', amount: 45000 },
124-
{ stage: 'Closed Won', amount: 225000 }
125-
]
107+
provider: 'object' as const,
108+
object: 'opportunity',
109+
aggregate: { field: 'amount', function: 'sum' as const, groupBy: 'stage' }
126110
}
127111
},
128112
},
129113
{
130114
title: 'Top Products',
131115
type: 'bar' as const,
132116
object: 'product',
133-
categoryField: 'name',
134-
valueField: 'sales',
117+
categoryField: 'category',
118+
valueField: 'price',
135119
aggregate: 'sum',
136120
layout: { x: 2, y: 3, w: 2, h: 2 },
137121
options: {
138-
xField: 'name',
139-
yField: 'sales',
122+
xField: 'category',
123+
yField: 'price',
140124
data: {
141-
provider: 'value' as const,
142-
items: [
143-
{ name: 'Workstation Pro Laptop', sales: 45000 },
144-
{ name: 'Implementation Service', sales: 32000 },
145-
{ name: 'Premium Support', sales: 21000 },
146-
{ name: 'Executive Mesh Chair', sales: 15000 }
147-
]
125+
provider: 'object' as const,
126+
object: 'product',
127+
aggregate: { field: 'price', function: 'sum' as const, groupBy: 'category' }
148128
}
149129
},
150130
},
151131

152-
// --- Row 4: Table ---
132+
// --- Row 4: Table (provider: 'object' — dynamic data) ---
153133
{
154134
title: 'Recent Opportunities',
155135
type: 'table' as const,
@@ -160,17 +140,11 @@ export const CrmDashboard = {
160140
{ header: 'Opportunity Name', accessorKey: 'name' },
161141
{ header: 'Amount', accessorKey: 'amount' },
162142
{ header: 'Stage', accessorKey: 'stage' },
163-
{ header: 'Close Date', accessorKey: 'date' }
143+
{ header: 'Close Date', accessorKey: 'close_date' }
164144
],
165145
data: {
166-
provider: 'value' as const,
167-
items: [
168-
{ name: 'Berlin Automation Project', amount: '$250,000', stage: 'Prospecting', date: '2024-09-01' },
169-
{ name: 'ObjectStack Enterprise License', amount: '$150,000', stage: 'Closed Won', date: '2024-01-15' },
170-
{ name: 'London Annual Renewal', amount: '$85,000', stage: 'Proposal', date: '2024-05-15' },
171-
{ name: 'SF Tower Expansion', amount: '$75,000', stage: 'Closed Won', date: '2024-02-28' },
172-
{ name: 'Global Fin Q1 Upsell', amount: '$45,000', stage: 'Negotiation', date: '2024-03-30' }
173-
]
146+
provider: 'object' as const,
147+
object: 'opportunity',
174148
}
175149
},
176150
},
@@ -193,6 +167,44 @@ export const CrmDashboard = {
193167
aggregate: { field: 'amount', function: 'sum' as const, groupBy: 'account' }
194168
}
195169
},
170+
},
171+
172+
// --- Row 6: Additional aggregate functions (avg, max) + cross-object ---
173+
{
174+
title: 'Avg Deal Size by Stage',
175+
type: 'line' as const,
176+
object: 'opportunity',
177+
categoryField: 'stage',
178+
valueField: 'amount',
179+
aggregate: 'avg',
180+
layout: { x: 0, y: 9, w: 2, h: 2 },
181+
options: {
182+
xField: 'stage',
183+
yField: 'amount',
184+
data: {
185+
provider: 'object' as const,
186+
object: 'opportunity',
187+
aggregate: { field: 'amount', function: 'avg' as const, groupBy: 'stage' }
188+
}
189+
},
190+
},
191+
{
192+
title: 'Orders by Status',
193+
type: 'bar' as const,
194+
object: 'order',
195+
categoryField: 'status',
196+
valueField: 'amount',
197+
aggregate: 'max',
198+
layout: { x: 2, y: 9, w: 2, h: 2 },
199+
options: {
200+
xField: 'status',
201+
yField: 'amount',
202+
data: {
203+
provider: 'object' as const,
204+
object: 'order',
205+
aggregate: { field: 'amount', function: 'max' as const, groupBy: 'status' }
206+
}
207+
},
196208
}
197209
]
198210
};

examples/crm/src/i18n/ar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const ar = {
9696
},
9797
dashboard: {
9898
title: 'نظرة عامة على CRM',
99-
widgets: { totalRevenue: 'إجمالي الإيرادات', activeDeals: 'الصفقات النشطة', winRate: 'معدل الفوز', avgDealSize: 'متوسط حجم الصفقة', revenueTrends: 'اتجاهات الإيرادات', leadSource: 'مصدر العملاء المحتملين', pipelineByStage: 'خط الأنابيب حسب المرحلة', topProducts: 'أفضل المنتجات', recentOpportunities: 'الفرص الأخيرة', revenueByAccount: 'الإيرادات حسب الحساب' },
99+
widgets: { totalRevenue: 'إجمالي الإيرادات', activeDeals: 'الصفقات النشطة', winRate: 'معدل الفوز', avgDealSize: 'متوسط حجم الصفقة', revenueTrends: 'اتجاهات الإيرادات', leadSource: 'مصدر العملاء المحتملين', pipelineByStage: 'خط الأنابيب حسب المرحلة', topProducts: 'أفضل المنتجات', recentOpportunities: 'الفرص الأخيرة', revenueByAccount: 'الإيرادات حسب الحساب', avgDealSizeByStage: 'متوسط حجم الصفقة حسب المرحلة', ordersByStatus: 'الطلبات حسب الحالة' },
100100
trendLabel: 'مقارنة بالشهر الماضي',
101101
columns: { opportunityName: 'اسم الفرصة', amount: 'المبلغ', stage: 'المرحلة', closeDate: 'تاريخ الإغلاق' },
102102
},

examples/crm/src/i18n/de.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const de = {
118118
},
119119
dashboard: {
120120
title: 'CRM-Übersicht',
121-
widgets: { totalRevenue: 'Gesamtumsatz', activeDeals: 'Aktive Geschäfte', winRate: 'Gewinnrate', avgDealSize: 'Ø Geschäftsgröße', revenueTrends: 'Umsatztrends', leadSource: 'Lead-Quelle', pipelineByStage: 'Pipeline nach Phase', topProducts: 'Top-Produkte', recentOpportunities: 'Aktuelle Chancen', revenueByAccount: 'Umsatz nach Konto' },
121+
widgets: { totalRevenue: 'Gesamtumsatz', activeDeals: 'Aktive Geschäfte', winRate: 'Gewinnrate', avgDealSize: 'Ø Geschäftsgröße', revenueTrends: 'Umsatztrends', leadSource: 'Lead-Quelle', pipelineByStage: 'Pipeline nach Phase', topProducts: 'Top-Produkte', recentOpportunities: 'Aktuelle Chancen', revenueByAccount: 'Umsatz nach Konto', avgDealSizeByStage: 'Ø Geschäftsgröße nach Phase', ordersByStatus: 'Bestellungen nach Status' },
122122
trendLabel: 'ggü. Vormonat',
123123
columns: { opportunityName: 'Chancenname', amount: 'Betrag', stage: 'Phase', closeDate: 'Abschlussdatum' },
124124
},

examples/crm/src/i18n/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ const en = {
381381
topProducts: 'Top Products',
382382
recentOpportunities: 'Recent Opportunities',
383383
revenueByAccount: 'Revenue by Account',
384+
avgDealSizeByStage: 'Avg Deal Size by Stage',
385+
ordersByStatus: 'Orders by Status',
384386
},
385387
trendLabel: 'vs last month',
386388
columns: {

examples/crm/src/i18n/es.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const es = {
118118
},
119119
dashboard: {
120120
title: 'Resumen CRM',
121-
widgets: { totalRevenue: 'Ingresos totales', activeDeals: 'Negocios activos', winRate: 'Tasa de éxito', avgDealSize: 'Tamaño promedio', revenueTrends: 'Tendencias de ingresos', leadSource: 'Fuente de leads', pipelineByStage: 'Pipeline por etapa', topProducts: 'Productos principales', recentOpportunities: 'Oportunidades recientes', revenueByAccount: 'Ingresos por cuenta' },
121+
widgets: { totalRevenue: 'Ingresos totales', activeDeals: 'Negocios activos', winRate: 'Tasa de éxito', avgDealSize: 'Tamaño promedio', revenueTrends: 'Tendencias de ingresos', leadSource: 'Fuente de leads', pipelineByStage: 'Pipeline por etapa', topProducts: 'Productos principales', recentOpportunities: 'Oportunidades recientes', revenueByAccount: 'Ingresos por cuenta', avgDealSizeByStage: 'Tamaño promedio por etapa', ordersByStatus: 'Pedidos por estado' },
122122
trendLabel: 'vs mes anterior',
123123
columns: { opportunityName: 'Nombre de oportunidad', amount: 'Monto', stage: 'Etapa', closeDate: 'Fecha de cierre' },
124124
},

examples/crm/src/i18n/fr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const fr = {
118118
},
119119
dashboard: {
120120
title: 'Aperçu CRM',
121-
widgets: { totalRevenue: 'Chiffre d\'affaires total', activeDeals: 'Affaires actives', winRate: 'Taux de réussite', avgDealSize: 'Taille moyenne', revenueTrends: 'Tendances du CA', leadSource: 'Source des leads', pipelineByStage: 'Pipeline par étape', topProducts: 'Produits phares', recentOpportunities: 'Opportunités récentes', revenueByAccount: 'CA par compte' },
121+
widgets: { totalRevenue: 'Chiffre d\'affaires total', activeDeals: 'Affaires actives', winRate: 'Taux de réussite', avgDealSize: 'Taille moyenne', revenueTrends: 'Tendances du CA', leadSource: 'Source des leads', pipelineByStage: 'Pipeline par étape', topProducts: 'Produits phares', recentOpportunities: 'Opportunités récentes', revenueByAccount: 'CA par compte', avgDealSizeByStage: 'Taille moyenne par étape', ordersByStatus: 'Commandes par statut' },
122122
trendLabel: 'vs mois précédent',
123123
columns: { opportunityName: 'Nom de l\'opportunité', amount: 'Montant', stage: 'Étape', closeDate: 'Date de clôture' },
124124
},

examples/crm/src/i18n/ja.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,8 @@ const ja = {
378378
topProducts: '人気商品',
379379
recentOpportunities: '最近の商談',
380380
revenueByAccount: '取引先別売上',
381+
avgDealSizeByStage: 'ステージ別平均取引規模',
382+
ordersByStatus: 'ステータス別注文',
381383
},
382384
trendLabel: '前月比',
383385
columns: {

examples/crm/src/i18n/ko.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const ko = {
118118
},
119119
dashboard: {
120120
title: 'CRM 개요',
121-
widgets: { totalRevenue: '총 매출', activeDeals: '활성 거래', winRate: '수주율', avgDealSize: '평균 거래 규모', revenueTrends: '매출 추이', leadSource: '리드 소스', pipelineByStage: '단계별 파이프라인', topProducts: '인기 제품', recentOpportunities: '최근 영업기회', revenueByAccount: '거래처별 매출' },
121+
widgets: { totalRevenue: '총 매출', activeDeals: '활성 거래', winRate: '수주율', avgDealSize: '평균 거래 규모', revenueTrends: '매출 추이', leadSource: '리드 소스', pipelineByStage: '단계별 파이프라인', topProducts: '인기 제품', recentOpportunities: '최근 영업기회', revenueByAccount: '거래처별 매출', avgDealSizeByStage: '단계별 평균 거래 규모', ordersByStatus: '상태별 주문' },
122122
trendLabel: '전월 대비',
123123
columns: { opportunityName: '영업기회명', amount: '금액', stage: '단계', closeDate: '종료일' },
124124
},

0 commit comments

Comments
 (0)