Skip to content

Commit 6f62aac

Browse files
authored
Merge pull request #846 from objectstack-ai/copilot/fix-dashboard-widgets-data-issues
2 parents 00c9160 + 7aa1345 commit 6f62aac

File tree

18 files changed

+309
-32
lines changed

18 files changed

+309
-32
lines changed

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
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
866866
- [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.
867867
- [x] **P1: Dashboard provider:'object' Crash & Blank Rendering Fixes** — Fixed 3 critical bugs causing all charts to be blank and tables to crash on provider:'object' dashboards: (1) DashboardRenderer `...options` spread was leaking provider config objects as `data` in data-table and pivot schemas — fixed by destructuring `data` out before spread, (2) DataTableRenderer and PivotTable now guard with `Array.isArray()` for graceful degradation when non-array data arrives, (3) ObjectChart now shows visible loading/warning messages instead of silently rendering blank when `dataSource` is missing. Also added provider:'object' support to DashboardGridLayout (charts, tables, pivots). 2 new regression tests.
868+
- [x] **P1: Dashboard Widget Data Blank — useDataScope/dataSource Injection Fix** — Fixed root cause of dashboard widgets showing blank data with no server requests: `useDataScope(undefined)` was returning the full context `dataSource` (service adapter) instead of `undefined` when no bind path was given, causing ObjectChart and all data components (ObjectKanban, ObjectGallery, ObjectTimeline, ObjectGrid) to treat the adapter as pre-bound data and skip async fetching. Fixed `useDataScope` to return `undefined` when no path is provided. Also improved ObjectChart fault tolerance: uses `useContext` directly instead of `useSchemaContext` (no throw without provider), validates `dataSource.find` is callable before invoking. 14 new tests (7 useDataScope + 7 ObjectChart data fetch/fault tolerance).
868869
- [x] **P1: URL-Driven Debug/Developer Panel** — Universal debug mode activated via `?__debug` URL parameter (amis devtools-style). `@object-ui/core`: exported `DebugFlags`, `DebugCollector` (perf/expr/event data collection, tree-shakeable), `parseDebugFlags()`, enhanced `isDebugEnabled()` (URL → globalThis → env resolution, SSR-safe). `@object-ui/react`: `useDebugMode` hook with URL detection, Ctrl+Shift+D shortcut, manual toggle; `SchemaRendererContext` extended with `debugFlags`; `SchemaRenderer` injects `data-debug-type`/`data-debug-id` attrs + reports render perf to `DebugCollector` when debug enabled. `@object-ui/components`: floating `DebugPanel` with 7 built-in tabs (Schema, Data, Perf, Expr, Events, Registry, Flags), plugin-extensible via `extraTabs`. Console `MetadataInspector` auto-opens when `?__debug` detected. Fine-grained sub-flags: `?__debug_schema`, `?__debug_perf`, `?__debug_data`, `?__debug_expr`, `?__debug_events`, `?__debug_registry`. 48 new tests.
869870

870871
### Ecosystem & Marketplace

packages/plugin-calendar/src/ObjectCalendar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
209209
}
210210
}
211211

212-
if (!dataSource) {
212+
if (!dataSource || typeof dataSource.find !== 'function') {
213213
throw new Error('DataSource required for object/api providers');
214214
}
215215

packages/plugin-calendar/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import './calendar-view-renderer';
2525

2626
// Register object-calendar component
2727
export const ObjectCalendarRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, data: _data, loading: _loading, ...props }) => {
28-
const { dataSource } = useSchemaContext();
28+
const { dataSource } = useSchemaContext() || {};
2929
return <ObjectCalendar schema={schema} dataSource={dataSource} {...props} />;
3030
};
3131

packages/plugin-charts/src/ObjectChart.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

2-
import React, { useState, useEffect } from 'react';
3-
import { useDataScope, useSchemaContext } from '@object-ui/react';
2+
import React, { useState, useEffect, useContext } from 'react';
3+
import { useDataScope, SchemaRendererContext } from '@object-ui/react';
44
import { ChartRenderer } from './ChartRenderer';
55
import { ComponentRegistry, extractRecords } from '@object-ui/core';
66

@@ -54,8 +54,8 @@ export { extractRecords } from '@object-ui/core';
5454

5555
export const ObjectChart = (props: any) => {
5656
const { schema } = props;
57-
const context = useSchemaContext();
58-
const dataSource = props.dataSource || context.dataSource;
57+
const context = useContext(SchemaRendererContext);
58+
const dataSource = props.dataSource || context?.dataSource;
5959
const boundData = useDataScope(schema.bind);
6060

6161
const [fetchedData, setFetchedData] = useState<any[]>([]);
@@ -64,7 +64,7 @@ export const ObjectChart = (props: any) => {
6464
useEffect(() => {
6565
let isMounted = true;
6666
const fetchData = async () => {
67-
if (!dataSource || !schema.objectName) return;
67+
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
6868
if (isMounted) setLoading(true);
6969
try {
7070
const results = await dataSource.find(schema.objectName, {
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* Tests for ObjectChart data fetching & fault tolerance.
3+
*
4+
* Verifies that ObjectChart:
5+
* - Calls dataSource.find() when objectName is set and no bound data
6+
* - Handles missing/invalid dataSource gracefully
7+
* - Works without a SchemaRendererProvider
8+
*/
9+
10+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11+
import { render, waitFor } from '@testing-library/react';
12+
import React from 'react';
13+
import { SchemaRendererProvider } from '@object-ui/react';
14+
import { ObjectChart } from '../ObjectChart';
15+
16+
// Suppress console.error from React error boundary / fetch errors
17+
const originalConsoleError = console.error;
18+
beforeEach(() => {
19+
console.error = vi.fn();
20+
});
21+
afterEach(() => {
22+
console.error = originalConsoleError;
23+
});
24+
25+
describe('ObjectChart data fetching', () => {
26+
it('should call dataSource.find when objectName is set and no bind path', async () => {
27+
const mockFind = vi.fn().mockResolvedValue([
28+
{ stage: 'Prospect', amount: 100 },
29+
{ stage: 'Proposal', amount: 200 },
30+
]);
31+
const dataSource = { find: mockFind };
32+
33+
render(
34+
<SchemaRendererProvider dataSource={dataSource}>
35+
<ObjectChart
36+
schema={{
37+
type: 'object-chart',
38+
objectName: 'opportunity',
39+
chartType: 'bar',
40+
xAxisKey: 'stage',
41+
series: [{ dataKey: 'amount' }],
42+
}}
43+
/>
44+
</SchemaRendererProvider>
45+
);
46+
47+
await waitFor(() => {
48+
expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
49+
});
50+
});
51+
52+
it('should NOT call dataSource.find when schema.data is provided', () => {
53+
const mockFind = vi.fn();
54+
const dataSource = { find: mockFind };
55+
56+
render(
57+
<SchemaRendererProvider dataSource={dataSource}>
58+
<ObjectChart
59+
schema={{
60+
type: 'object-chart',
61+
objectName: 'opportunity',
62+
chartType: 'bar',
63+
data: [{ stage: 'A', amount: 100 }],
64+
xAxisKey: 'stage',
65+
series: [{ dataKey: 'amount' }],
66+
}}
67+
/>
68+
</SchemaRendererProvider>
69+
);
70+
71+
expect(mockFind).not.toHaveBeenCalled();
72+
});
73+
74+
it('should apply aggregation to fetched data', async () => {
75+
const mockFind = vi.fn().mockResolvedValue([
76+
{ stage: 'Prospect', amount: 100 },
77+
{ stage: 'Prospect', amount: 200 },
78+
{ stage: 'Proposal', amount: 300 },
79+
]);
80+
const dataSource = { find: mockFind };
81+
82+
const { container } = render(
83+
<SchemaRendererProvider dataSource={dataSource}>
84+
<ObjectChart
85+
schema={{
86+
type: 'object-chart',
87+
objectName: 'opportunity',
88+
chartType: 'bar',
89+
xAxisKey: 'stage',
90+
series: [{ dataKey: 'amount' }],
91+
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
92+
}}
93+
/>
94+
</SchemaRendererProvider>
95+
);
96+
97+
await waitFor(() => {
98+
expect(mockFind).toHaveBeenCalled();
99+
});
100+
});
101+
});
102+
103+
describe('ObjectChart fault tolerance', () => {
104+
it('should not crash when dataSource has no find method', () => {
105+
const { container } = render(
106+
<SchemaRendererProvider dataSource={{}}>
107+
<ObjectChart
108+
schema={{
109+
type: 'object-chart',
110+
objectName: 'opportunity',
111+
chartType: 'bar',
112+
xAxisKey: 'stage',
113+
series: [{ dataKey: 'amount' }],
114+
}}
115+
/>
116+
</SchemaRendererProvider>
117+
);
118+
119+
// Should render without crashing
120+
expect(container).toBeDefined();
121+
});
122+
123+
it('should not crash when rendered outside SchemaRendererProvider', () => {
124+
const { container } = render(
125+
<ObjectChart
126+
schema={{
127+
type: 'object-chart',
128+
chartType: 'bar',
129+
xAxisKey: 'stage',
130+
series: [{ dataKey: 'amount' }],
131+
}}
132+
/>
133+
);
134+
135+
// Should render without crashing
136+
expect(container).toBeDefined();
137+
});
138+
139+
it('should show "No data source available" when no dataSource and objectName set', () => {
140+
const { container } = render(
141+
<ObjectChart
142+
schema={{
143+
type: 'object-chart',
144+
objectName: 'opportunity',
145+
chartType: 'bar',
146+
xAxisKey: 'stage',
147+
series: [{ dataKey: 'amount' }],
148+
}}
149+
/>
150+
);
151+
152+
expect(container.textContent).toContain('No data source available');
153+
});
154+
155+
it('should use dataSource prop over context when both are present', async () => {
156+
const contextFind = vi.fn().mockResolvedValue([]);
157+
const propFind = vi.fn().mockResolvedValue([{ stage: 'A', amount: 1 }]);
158+
159+
render(
160+
<SchemaRendererProvider dataSource={{ find: contextFind }}>
161+
<ObjectChart
162+
dataSource={{ find: propFind }}
163+
schema={{
164+
type: 'object-chart',
165+
objectName: 'opportunity',
166+
chartType: 'bar',
167+
xAxisKey: 'stage',
168+
series: [{ dataKey: 'amount' }],
169+
}}
170+
/>
171+
</SchemaRendererProvider>
172+
);
173+
174+
await waitFor(() => {
175+
expect(propFind).toHaveBeenCalled();
176+
});
177+
expect(contextFind).not.toHaveBeenCalled();
178+
});
179+
});

packages/plugin-detail/src/RelatedList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
3838
React.useEffect(() => {
3939
if (api && !data.length) {
4040
setLoading(true);
41-
if (dataSource) {
41+
if (dataSource && typeof dataSource.find === 'function') {
4242
dataSource.find(api).then((result) => {
4343
const items = Array.isArray(result)
4444
? result

packages/plugin-gantt/src/ObjectGantt.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
168168
return;
169169
}
170170

171-
if (!dataSource) {
171+
if (!dataSource || typeof dataSource.find !== 'function') {
172172
throw new Error('DataSource required for object/api providers');
173173
}
174174

@@ -178,7 +178,6 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
178178
$filter: schema.filter,
179179
$orderby: convertSortToQueryParams(schema.sort),
180180
});
181-
182181
let items: any[] = extractRecords(result);
183182
setData(items);
184183
} else if (dataConfig?.provider === 'api') {

packages/plugin-gantt/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type { GanttViewProps, GanttTask, GanttViewMode } from './GanttView';
2020

2121
// Register component
2222
export const ObjectGanttRenderer: React.FC<{ schema: any }> = ({ schema }) => {
23-
const { dataSource } = useSchemaContext();
23+
const { dataSource } = useSchemaContext() || {};
2424
return <ObjectGantt schema={schema} dataSource={dataSource} />;
2525
};
2626

packages/plugin-kanban/src/ObjectKanban.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
5858
useEffect(() => {
5959
let isMounted = true;
6060
const fetchData = async () => {
61-
if (!dataSource || !schema.objectName) return;
61+
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
6262
if (isMounted) setLoading(true);
6363
try {
6464
const results = await dataSource.find(schema.objectName, {

packages/plugin-list/src/ObjectGallery.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import React, { useState, useEffect, useCallback, useMemo } from 'react';
10-
import { useDataScope, useSchemaContext, useNavigationOverlay } from '@object-ui/react';
9+
import React, { useState, useEffect, useCallback, useMemo, useContext } from 'react';
10+
import { useDataScope, SchemaRendererContext, useNavigationOverlay } from '@object-ui/react';
1111
import { ComponentRegistry } from '@object-ui/core';
1212
import { cn, Card, CardContent, NavigationOverlay } from '@object-ui/components';
1313
import type { GalleryConfig, ViewNavigationConfig, GroupingConfig } from '@object-ui/types';
@@ -52,8 +52,8 @@ const ASPECT_CLASSES: Record<NonNullable<GalleryConfig['cardSize']>, string> = {
5252

5353
export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
5454
const { schema } = props;
55-
const context = useSchemaContext();
56-
const dataSource = props.dataSource || context.dataSource;
55+
const context = useContext(SchemaRendererContext);
56+
const dataSource = props.dataSource || context?.dataSource;
5757
const boundData = useDataScope(schema.bind);
5858

5959
const [fetchedData, setFetchedData] = useState<Record<string, unknown>[]>([]);
@@ -83,7 +83,7 @@ export const ObjectGallery: React.FC<ObjectGalleryProps> = (props) => {
8383
}
8484

8585
const fetchData = async () => {
86-
if (!dataSource || !schema.objectName) return;
86+
if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
8787
if (isMounted) setLoading(true);
8888
try {
8989
const results = await dataSource.find(schema.objectName, {

0 commit comments

Comments
 (0)