Skip to content

Commit 35eac0a

Browse files
authored
Merge pull request #302 from objectstack-ai/copilot/fix-dashboard-metadata-compliance-issues
2 parents bf6fa88 + ab5d1aa commit 35eac0a

30 files changed

Lines changed: 379 additions & 93 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- **Fix dashboard metadata columns/options compliance across all packages** — All `table`-type
12+
dashboard widgets now include `options.columns` specifying the fields to display, preventing
13+
rendering errors (`Cannot read properties of undefined (reading 'columns')`). Additionally,
14+
converted all widget `filter` fields from the non-compliant ObjectQL array format
15+
(`['field', 'op', 'value']`) to the MongoDB-style `FilterCondition` record format
16+
(`{ field: { $op: value } }`) expected by `DashboardSchema`. All 15 dashboards across CRM,
17+
Finance, Support, Marketing, Products, HR, Analytics, Integration, Community, Healthcare,
18+
Real-Estate, Financial-Services, and Education now pass `DashboardSchema.parse()` validation
19+
at module load time.
20+
- Affected files: `sales.dashboard.ts`, `crm.dashboard.ts`, `executive.dashboard.ts`,
21+
`support.dashboard.ts`, `finance.dashboard.ts`, `marketing.dashboard.ts`, `cpq.dashboard.ts`,
22+
`hr.dashboard.ts`, `analytics.dashboard.ts`, `integration.dashboard.ts`,
23+
`community.dashboard.ts`, `healthcare.dashboard.ts`, `brokerage.dashboard.ts`,
24+
`wealth_management.dashboard.ts`, `admissions.dashboard.ts`
25+
- Updated test expectations in CRM, Support, HR to reflect successful validation
26+
- Added dashboard validation tests with `options.columns` checks for all 12 packages that have
27+
`table`-type widgets: CRM (3 dashboards), Support, Finance, Marketing, Products, Analytics,
28+
Integration, Community, Healthcare, Real-Estate, Financial-Services, Education (HR dashboards
29+
contain no `table` widgets; HR tests assert only successful module load / schema validation)
30+
- Extracted shared `assertTableWidgetsHaveColumns()` test helper to
31+
`packages/core/__tests__/helpers/dashboard-test-utils.ts`
32+
- Also audited `*_enhanced.ts` (6 files) and `*.blank_page.ts` (3 files) — confirmed compliant
33+
1034
### Added
1135
- **Load `@objectstack/plugin-auth` in Vercel deployment** — The Auth Plugin (better-auth based)
1236
is now registered in the Vercel serverless handler (`api/[[...route]].ts`), providing
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { assertTableWidgetsHaveColumns } from '../../../../core/__tests__/helpers/dashboard-test-utils';
3+
4+
describe('Analytics Dashboard Schema Compliance', () => {
5+
describe('AnalyticsDashboard', () => {
6+
it('should pass module-level DashboardSchema validation', async () => {
7+
const mod = await import('../../../src/analytics.dashboard');
8+
expect(mod.AnalyticsDashboard).toBeDefined();
9+
});
10+
11+
it('should have options.columns on table widgets', async () => {
12+
const mod = await import('../../../src/analytics.dashboard');
13+
assertTableWidgetsHaveColumns(mod.AnalyticsDashboard);
14+
});
15+
});
16+
});

packages/analytics/src/analytics.dashboard.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const AnalyticsDashboard = {
2424
type: 'metric' as const,
2525
object: 'analytics_dashboard',
2626
aggregate: 'count' as const,
27-
filter: ['is_default', '=', true],
27+
filter: { is_default: true },
2828
layout: { x: 3, y: 0, w: 3, h: 2 }
2929
},
3030
{
@@ -33,7 +33,7 @@ export const AnalyticsDashboard = {
3333
type: 'kpi' as const,
3434
object: 'kpi',
3535
aggregate: 'count' as const,
36-
filter: ['trend', '=', 'declining'],
36+
filter: { trend: 'declining' },
3737
layout: { x: 6, y: 0, w: 3, h: 2 }
3838
},
3939
{
@@ -42,7 +42,7 @@ export const AnalyticsDashboard = {
4242
type: 'metric' as const,
4343
object: 'data_source',
4444
aggregate: 'count' as const,
45-
filter: ['sync_status', '=', 'connected'],
45+
filter: { sync_status: 'connected' },
4646
layout: { x: 9, y: 0, w: 3, h: 2 }
4747
},
4848
{
@@ -79,8 +79,11 @@ export const AnalyticsDashboard = {
7979
type: 'table' as const,
8080
aggregate: 'count' as const,
8181
object: 'report',
82-
filter: ['last_run_at', '>=', 'LAST_7_DAYS'],
83-
layout: { x: 6, y: 6, w: 6, h: 4 }
82+
filter: { last_run_at: { $gte: 'LAST_7_DAYS' } },
83+
layout: { x: 6, y: 6, w: 6, h: 4 },
84+
options: {
85+
columns: ['name', 'report_type', 'last_run_at', 'run_count']
86+
}
8487
}
8588
]
8689
} satisfies Dashboard;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { assertTableWidgetsHaveColumns } from '../../../../core/__tests__/helpers/dashboard-test-utils';
3+
4+
describe('Community Dashboard Schema Compliance', () => {
5+
describe('CommunityDashboard', () => {
6+
it('should pass module-level DashboardSchema validation', async () => {
7+
const mod = await import('../../../src/community.dashboard');
8+
expect(mod.CommunityDashboard).toBeDefined();
9+
});
10+
11+
it('should have options.columns on table widgets', async () => {
12+
const mod = await import('../../../src/community.dashboard');
13+
assertTableWidgetsHaveColumns(mod.CommunityDashboard);
14+
});
15+
});
16+
});

packages/community/src/community.dashboard.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const CommunityDashboard = {
1616
type: 'metric' as const,
1717
object: 'topic',
1818
aggregate: 'count' as const,
19-
filter: ['status', '=', 'open'],
19+
filter: { status: 'open' },
2020
layout: { x: 0, y: 0, w: 3, h: 2 }
2121
},
2222
{
@@ -76,8 +76,11 @@ export const CommunityDashboard = {
7676
type: 'table' as const,
7777
object: 'topic',
7878
aggregate: 'count' as const,
79-
filter: ['last_activity_at', '>=', 'LAST_7_DAYS'],
80-
layout: { x: 6, y: 6, w: 6, h: 4 }
79+
filter: { last_activity_at: { $gte: 'LAST_7_DAYS' } },
80+
layout: { x: 6, y: 6, w: 6, h: 4 },
81+
options: {
82+
columns: ['title', 'status', 'category_id', 'reply_count', 'last_activity_at']
83+
}
8184
}
8285
]
8386
} satisfies Dashboard;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { expect } from 'vitest';
2+
import type { Dashboard } from '@objectstack/spec/ui';
3+
4+
/**
5+
* Assert every `table`-type widget in a dashboard has `options.columns`
6+
* defined as a non-empty string array.
7+
*
8+
* @param dashboard The dashboard definition to inspect.
9+
* @param expectAtLeastOne If `true` (default), also asserts at least one
10+
* table widget exists — preventing the test from silently passing on
11+
* dashboards that happen to have zero table widgets.
12+
*/
13+
export function assertTableWidgetsHaveColumns(
14+
dashboard: Dashboard,
15+
expectAtLeastOne = true,
16+
): void {
17+
const tableWidgets = dashboard.widgets.filter((w: any) => w.type === 'table');
18+
if (expectAtLeastOne) {
19+
expect(tableWidgets.length).toBeGreaterThan(0);
20+
}
21+
for (const w of tableWidgets as any[]) {
22+
expect(w.options, `table widget "${w.id}" should have options`).toBeDefined();
23+
expect(w.options.columns, `table widget "${w.id}" should have options.columns`).toBeDefined();
24+
expect(Array.isArray(w.options.columns)).toBe(true);
25+
expect(w.options.columns.length).toBeGreaterThan(0);
26+
}
27+
}

packages/crm/__tests__/unit/schemas/ui-schema.test.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { PipelineFunnelChart } from '../../../src/pipeline_funnel.chart';
1212
import { RevenueTrendChart } from '../../../src/revenue_trend.chart';
1313
import { LogACallAction, ConvertLeadAction, CreateFollowUpTaskAction, SendQuoteAction } from '../../../src/crm_actions.action_ui';
1414
import { PipelineWidget } from '../../../src/pipeline_widget.widget';
15+
import { assertTableWidgetsHaveColumns } from '../../../../core/__tests__/helpers/dashboard-test-utils';
1516

1617
describe('CRM UI Schema Compliance', () => {
1718
describe('AccountPage', () => {
@@ -47,10 +48,39 @@ describe('CRM UI Schema Compliance', () => {
4748

4849
describe('CrmDashboard', () => {
4950
// CrmDashboard module calls DashboardSchema.parse() at load time.
50-
// The dashboard widget filter format currently uses arrays (ObjectQL style)
51-
// which does not match the DashboardSchema expectation of record/object.
52-
it('should fail module-level DashboardSchema validation (known schema mismatch)', async () => {
53-
await expect(() => import('../../../src/crm.dashboard')).rejects.toThrow();
51+
// Filters now use MongoDB-style FilterCondition format.
52+
it('should pass module-level DashboardSchema validation', async () => {
53+
const mod = await import('../../../src/crm.dashboard');
54+
expect(mod.CrmDashboard).toBeDefined();
55+
});
56+
57+
it('should have options.columns on table widgets', async () => {
58+
const mod = await import('../../../src/crm.dashboard');
59+
assertTableWidgetsHaveColumns(mod.CrmDashboard);
60+
});
61+
});
62+
63+
describe('SalesDashboard', () => {
64+
it('should pass module-level DashboardSchema validation', async () => {
65+
const mod = await import('../../../src/sales.dashboard');
66+
expect(mod.SalesDashboard).toBeDefined();
67+
});
68+
69+
it('should have options.columns on table widgets', async () => {
70+
const mod = await import('../../../src/sales.dashboard');
71+
assertTableWidgetsHaveColumns(mod.SalesDashboard);
72+
});
73+
});
74+
75+
describe('ExecutiveDashboard', () => {
76+
it('should pass module-level DashboardSchema validation', async () => {
77+
const mod = await import('../../../src/executive.dashboard');
78+
expect(mod.ExecutiveDashboard).toBeDefined();
79+
});
80+
81+
it('should have options.columns on table widgets', async () => {
82+
const mod = await import('../../../src/executive.dashboard');
83+
assertTableWidgetsHaveColumns(mod.ExecutiveDashboard);
5484
});
5585
});
5686

packages/crm/src/crm.dashboard.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const CrmDashboard = {
1717
object: 'opportunity',
1818
valueField: 'amount',
1919
aggregate: 'sum' as const,
20-
filter: ['stage', '!=', 'closed_lost'],
20+
filter: { stage: { $ne: 'closed_lost' } },
2121
layout: { x: 0, y: 0, w: 3, h: 2 }
2222
},
2323
{
@@ -26,7 +26,7 @@ export const CrmDashboard = {
2626
type: 'metric' as const,
2727
object: 'opportunity',
2828
aggregate: 'count' as const,
29-
filter: ['stage', '!=', 'closed_lost'],
29+
filter: { stage: { $ne: 'closed_lost' } },
3030
layout: { x: 3, y: 0, w: 3, h: 2 }
3131
},
3232
{
@@ -36,7 +36,7 @@ export const CrmDashboard = {
3636
object: 'opportunity',
3737
valueField: 'amount',
3838
aggregate: 'sum' as const,
39-
filter: ['stage', '=', 'closed_won'],
39+
filter: { stage: 'closed_won' },
4040
layout: { x: 6, y: 0, w: 3, h: 2 }
4141
},
4242
{
@@ -84,8 +84,11 @@ export const CrmDashboard = {
8484
type: 'table' as const,
8585
aggregate: 'count' as const,
8686
object: 'opportunity',
87-
filter: ['stage', '!=', 'closed_lost'],
88-
layout: { x: 6, y: 6, w: 6, h: 4 }
87+
filter: { stage: { $ne: 'closed_lost' } },
88+
layout: { x: 6, y: 6, w: 6, h: 4 },
89+
options: {
90+
columns: ['name', 'amount', 'stage', 'close_date']
91+
}
8992
}
9093
]
9194
} satisfies Dashboard;

packages/crm/src/executive.dashboard.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const ExecutiveDashboard = {
1717
object: 'opportunity',
1818
valueField: 'amount',
1919
aggregate: 'sum' as const,
20-
filter: ['stage', '!=', 'closed_lost'],
20+
filter: { stage: { $ne: 'closed_lost' } },
2121
layout: { x: 0, y: 0, w: 3, h: 2 }
2222
},
2323
{
@@ -27,7 +27,7 @@ export const ExecutiveDashboard = {
2727
object: 'opportunity',
2828
valueField: 'amount',
2929
aggregate: 'sum' as const,
30-
filter: ['stage', '=', 'closed_won'],
30+
filter: { stage: 'closed_won' },
3131
layout: { x: 3, y: 0, w: 3, h: 2 }
3232
},
3333
{
@@ -36,7 +36,7 @@ export const ExecutiveDashboard = {
3636
type: 'metric' as const,
3737
object: 'case',
3838
aggregate: 'count' as const,
39-
filter: ['status', '!=', 'closed'],
39+
filter: { status: { $ne: 'closed' } },
4040
layout: { x: 6, y: 0, w: 3, h: 2 }
4141
},
4242
{
@@ -45,7 +45,7 @@ export const ExecutiveDashboard = {
4545
type: 'metric' as const,
4646
object: 'employee',
4747
aggregate: 'count' as const,
48-
filter: ['employment_status', '=', 'active'],
48+
filter: { employment_status: 'active' },
4949
layout: { x: 9, y: 0, w: 3, h: 2 }
5050
},
5151
{
@@ -66,7 +66,7 @@ export const ExecutiveDashboard = {
6666
categoryField: 'close_date',
6767
valueField: 'amount',
6868
aggregate: 'sum' as const,
69-
filter: ['stage', '=', 'closed_won'],
69+
filter: { stage: 'closed_won' },
7070
layout: { x: 6, y: 2, w: 6, h: 4 }
7171
},
7272
{
@@ -84,8 +84,11 @@ export const ExecutiveDashboard = {
8484
type: 'table' as const,
8585
aggregate: 'count' as const,
8686
object: 'opportunity',
87-
filter: ['stage', '!=', 'closed_lost'],
88-
layout: { x: 6, y: 6, w: 6, h: 4 }
87+
filter: { stage: { $ne: 'closed_lost' } },
88+
layout: { x: 6, y: 6, w: 6, h: 4 },
89+
options: {
90+
columns: ['name', 'amount', 'stage', 'close_date']
91+
}
8992
}
9093
]
9194
} satisfies Dashboard;

0 commit comments

Comments
 (0)