Skip to content

Commit 3d92084

Browse files
Copilothotlong
andcommitted
fix: useDataScope returns undefined when no path given, preventing dashboard widgets from blocking data fetch
Previously, useDataScope(undefined) returned the full context.dataSource (typically a service adapter), which caused ObjectChart and other data components to skip async data fetching because boundData was truthy. Also improved ObjectChart fault tolerance: - Uses useContext directly instead of useSchemaContext (no throw without provider) - Validates dataSource.find is callable before invoking Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 63c9aa5 commit 3d92084

4 files changed

Lines changed: 277 additions & 6 deletions

File tree

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/react/src/context/SchemaRendererContext.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export const useSchemaContext = () => {
4141
export const useDataScope = (path?: string) => {
4242
const context = useContext(SchemaRendererContext);
4343
const dataSource = context?.dataSource;
44-
if (!dataSource || !path) return dataSource;
44+
if (!path) return undefined;
45+
if (!dataSource) return undefined;
4546
// Simple path resolution for now. In real app might be more complex
4647
return path.split('.').reduce((acc, part) => acc && acc[part], dataSource);
4748
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Tests for useDataScope hook — verifies correct scoping behavior.
3+
*/
4+
5+
import { describe, it, expect } from 'vitest';
6+
import { renderHook } from '@testing-library/react';
7+
import React from 'react';
8+
import { SchemaRendererProvider, useDataScope } from '../SchemaRendererContext';
9+
10+
describe('useDataScope', () => {
11+
it('returns undefined when no path is provided', () => {
12+
const wrapper = ({ children }: { children: React.ReactNode }) => (
13+
<SchemaRendererProvider dataSource={{ users: [1, 2, 3] }}>
14+
{children}
15+
</SchemaRendererProvider>
16+
);
17+
18+
const { result } = renderHook(() => useDataScope(undefined), { wrapper });
19+
20+
expect(result.current).toBeUndefined();
21+
});
22+
23+
it('returns undefined when path is empty string', () => {
24+
const wrapper = ({ children }: { children: React.ReactNode }) => (
25+
<SchemaRendererProvider dataSource={{ users: [1, 2, 3] }}>
26+
{children}
27+
</SchemaRendererProvider>
28+
);
29+
30+
const { result } = renderHook(() => useDataScope(''), { wrapper });
31+
32+
expect(result.current).toBeUndefined();
33+
});
34+
35+
it('returns scoped data when a valid path is given', () => {
36+
const wrapper = ({ children }: { children: React.ReactNode }) => (
37+
<SchemaRendererProvider dataSource={{ users: [{ name: 'Alice' }] }}>
38+
{children}
39+
</SchemaRendererProvider>
40+
);
41+
42+
const { result } = renderHook(() => useDataScope('users'), { wrapper });
43+
44+
expect(result.current).toEqual([{ name: 'Alice' }]);
45+
});
46+
47+
it('resolves nested paths', () => {
48+
const wrapper = ({ children }: { children: React.ReactNode }) => (
49+
<SchemaRendererProvider dataSource={{ app: { settings: { theme: 'dark' } } }}>
50+
{children}
51+
</SchemaRendererProvider>
52+
);
53+
54+
const { result } = renderHook(() => useDataScope('app.settings.theme'), { wrapper });
55+
56+
expect(result.current).toBe('dark');
57+
});
58+
59+
it('returns undefined for non-existent path', () => {
60+
const wrapper = ({ children }: { children: React.ReactNode }) => (
61+
<SchemaRendererProvider dataSource={{ users: [] }}>
62+
{children}
63+
</SchemaRendererProvider>
64+
);
65+
66+
const { result } = renderHook(() => useDataScope('nonexistent'), { wrapper });
67+
68+
expect(result.current).toBeUndefined();
69+
});
70+
71+
it('returns undefined when no SchemaRendererProvider is present', () => {
72+
const { result } = renderHook(() => useDataScope('users'));
73+
74+
expect(result.current).toBeUndefined();
75+
});
76+
77+
it('does not return the adapter/service object when no path is given', () => {
78+
// Simulate the real scenario: dataSource is a service adapter with methods
79+
const adapter = { find: () => {}, create: () => {}, update: () => {} };
80+
const wrapper = ({ children }: { children: React.ReactNode }) => (
81+
<SchemaRendererProvider dataSource={adapter}>
82+
{children}
83+
</SchemaRendererProvider>
84+
);
85+
86+
const { result } = renderHook(() => useDataScope(undefined), { wrapper });
87+
88+
// Should NOT return the adapter — that would prevent ObjectChart from fetching
89+
expect(result.current).toBeUndefined();
90+
});
91+
});

0 commit comments

Comments
 (0)