Skip to content

Commit 51f9ac3

Browse files
authored
Merge pull request #568 from objectstack-ai/copilot/fix-report-viewer-data-issue
2 parents f16b4bf + 67d4d5b commit 51f9ac3

2 files changed

Lines changed: 293 additions & 2 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* ReportView Integration Tests
3+
*
4+
* Tests ReportView component with data loading from adapter
5+
*/
6+
7+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
8+
import { render, screen, waitFor } from '@testing-library/react';
9+
import '@testing-library/jest-dom';
10+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
11+
import { ReportView } from '../components/ReportView';
12+
import { MetadataProvider } from '../context/MetadataProvider';
13+
import { startMockServer, stopMockServer, getDriver } from '../mocks/server';
14+
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
15+
import type { DataSource } from '@object-ui/types';
16+
17+
describe('ReportView Data Loading', () => {
18+
let adapter: ObjectStackAdapter;
19+
20+
beforeAll(async () => {
21+
await startMockServer();
22+
adapter = new ObjectStackAdapter({
23+
baseUrl: '',
24+
autoReconnect: false,
25+
});
26+
await adapter.connect();
27+
28+
// Seed test data for the opportunity object
29+
const driver = getDriver();
30+
await driver.insert('opportunity', {
31+
id: '1',
32+
name: 'Deal Alpha',
33+
amount: 50000,
34+
stage: 'Proposal',
35+
close_date: '2024-03-31',
36+
});
37+
await driver.insert('opportunity', {
38+
id: '2',
39+
name: 'Deal Beta',
40+
amount: 75000,
41+
stage: 'Negotiation',
42+
close_date: '2024-04-15',
43+
});
44+
await driver.insert('opportunity', {
45+
id: '3',
46+
name: 'Deal Gamma',
47+
amount: 100000,
48+
stage: 'Closed Won',
49+
close_date: '2024-02-28',
50+
});
51+
});
52+
53+
afterAll(() => {
54+
stopMockServer();
55+
});
56+
57+
it('should load and display report data from objectName', async () => {
58+
render(
59+
<MemoryRouter initialEntries={['/apps/crm_app/report/sales_performance_q1']}>
60+
<MetadataProvider adapter={adapter}>
61+
<Routes>
62+
<Route
63+
path="/apps/:appName/report/:reportName"
64+
element={<ReportView dataSource={adapter as DataSource} />}
65+
/>
66+
</Routes>
67+
</MetadataProvider>
68+
</MemoryRouter>
69+
);
70+
71+
// Wait for metadata to load
72+
await waitFor(
73+
() => {
74+
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
75+
},
76+
{ timeout: 3000 }
77+
);
78+
79+
// Check that the report viewer is rendered
80+
await waitFor(
81+
() => {
82+
expect(screen.getByText(/Q1 Sales Performance/i)).toBeInTheDocument();
83+
},
84+
{ timeout: 3000 }
85+
);
86+
87+
// The report should render with data (checking for toolbar presence)
88+
expect(screen.getByRole('button', { name: /print/i })).toBeInTheDocument();
89+
});
90+
91+
it('should handle inline data in report definition', async () => {
92+
// Mock adapter for this test
93+
const mockAdapter = {
94+
...adapter,
95+
find: vi.fn().mockResolvedValue({
96+
data: [],
97+
total: 0,
98+
page: 1,
99+
pageSize: 0,
100+
hasMore: false,
101+
}),
102+
} as unknown as DataSource;
103+
104+
render(
105+
<MemoryRouter initialEntries={['/apps/test_app/report/inline_data_report']}>
106+
<MetadataProvider adapter={adapter}>
107+
<Routes>
108+
<Route
109+
path="/apps/:appName/report/:reportName"
110+
element={<ReportView dataSource={mockAdapter} />}
111+
/>
112+
</Routes>
113+
</MetadataProvider>
114+
</MemoryRouter>
115+
);
116+
117+
// Wait for metadata to load
118+
await waitFor(
119+
() => {
120+
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
121+
},
122+
{ timeout: 3000 }
123+
);
124+
125+
// If report has inline data, adapter.find should NOT be called
126+
// (This test would need a report with inline data in the metadata)
127+
});
128+
129+
it('should display loading state while fetching data', async () => {
130+
// Create a mock adapter that delays the response
131+
const delayedAdapter = {
132+
...adapter,
133+
find: vi.fn().mockImplementation(
134+
() =>
135+
new Promise((resolve) =>
136+
setTimeout(
137+
() =>
138+
resolve({
139+
data: [{ id: '1', name: 'Test' }],
140+
total: 1,
141+
page: 1,
142+
pageSize: 1,
143+
hasMore: false,
144+
}),
145+
100
146+
)
147+
)
148+
),
149+
} as unknown as DataSource;
150+
151+
render(
152+
<MemoryRouter initialEntries={['/apps/crm_app/report/sales_performance_q1']}>
153+
<MetadataProvider adapter={adapter}>
154+
<Routes>
155+
<Route
156+
path="/apps/:appName/report/:reportName"
157+
element={<ReportView dataSource={delayedAdapter} />}
158+
/>
159+
</Routes>
160+
</MetadataProvider>
161+
</MemoryRouter>
162+
);
163+
164+
// Initially, we should see metadata loading
165+
await waitFor(
166+
() => {
167+
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
168+
},
169+
{ timeout: 3000 }
170+
);
171+
172+
// Then the report should load
173+
await waitFor(
174+
() => {
175+
expect(screen.getByText(/Q1 Sales Performance/i)).toBeInTheDocument();
176+
},
177+
{ timeout: 3000 }
178+
);
179+
});
180+
181+
it('should handle missing report gracefully', async () => {
182+
render(
183+
<MemoryRouter initialEntries={['/apps/crm_app/report/nonexistent_report']}>
184+
<MetadataProvider adapter={adapter}>
185+
<Routes>
186+
<Route
187+
path="/apps/:appName/report/:reportName"
188+
element={<ReportView dataSource={adapter as DataSource} />}
189+
/>
190+
</Routes>
191+
</MetadataProvider>
192+
</MemoryRouter>
193+
);
194+
195+
// Wait for metadata to load
196+
await waitFor(
197+
() => {
198+
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
199+
},
200+
{ timeout: 3000 }
201+
);
202+
203+
// Should show "Report Not Found" message
204+
await waitFor(
205+
() => {
206+
expect(screen.getByText(/Report Not Found/i)).toBeInTheDocument();
207+
},
208+
{ timeout: 3000 }
209+
);
210+
});
211+
});

apps/console/src/components/ReportView.tsx

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/componen
55
import { PenLine, ChevronLeft, BarChart3, Loader2 } from 'lucide-react';
66
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
77
import { useMetadata } from '../context/MetadataProvider';
8+
import type { DataSource } from '@object-ui/types';
89

910
// Mock fields for the builder since we don't have a dynamic schema provider here yet
1011
const MOCK_FIELDS = [
@@ -18,7 +19,7 @@ const MOCK_FIELDS = [
1819
{ name: 'amount', label: 'Amount', type: 'currency' },
1920
];
2021

21-
export function ReportView({ dataSource: _dataSource }: { dataSource?: any }) {
22+
export function ReportView({ dataSource }: { dataSource?: DataSource }) {
2223
const { reportName } = useParams<{ reportName: string }>();
2324
const { showDebug, toggleDebug } = useMetadataInspector();
2425
const [isEditing, setIsEditing] = useState(false);
@@ -28,11 +29,88 @@ export function ReportView({ dataSource: _dataSource }: { dataSource?: any }) {
2829
const initialReport = reports?.find((r: any) => r.name === reportName);
2930
const [reportData, setReportData] = useState(initialReport);
3031

32+
// State for report runtime data
33+
const [reportRuntimeData, setReportRuntimeData] = useState<any[]>([]);
34+
const [dataLoading, setDataLoading] = useState(false);
35+
3136
// Sync reportData when metadata finishes loading or reportName changes
3237
useEffect(() => {
3338
setReportData(initialReport);
3439
}, [initialReport]);
3540

41+
// Load report runtime data when report definition changes
42+
useEffect(() => {
43+
if (!reportData || !dataSource) {
44+
setReportRuntimeData([]);
45+
return;
46+
}
47+
48+
// If report has inline data, use it directly
49+
if (reportData.data && Array.isArray(reportData.data)) {
50+
setReportRuntimeData(reportData.data);
51+
return;
52+
}
53+
54+
// If report has a dataSource config, fetch data using it
55+
if (reportData.dataSource) {
56+
const fetchDataFromSource = async () => {
57+
setDataLoading(true);
58+
try {
59+
// Use the dataSource configuration to fetch data
60+
const resource = reportData.dataSource.object || reportData.dataSource.resource;
61+
if (!resource) {
62+
console.warn('ReportView: dataSource missing object/resource property');
63+
setReportRuntimeData([]);
64+
return;
65+
}
66+
67+
const result = await dataSource.find(resource, {
68+
$filter: reportData.dataSource.filter,
69+
$orderby: reportData.dataSource.sort,
70+
$top: reportData.dataSource.limit,
71+
});
72+
73+
setReportRuntimeData(result.data || []);
74+
} catch (error) {
75+
console.error('ReportView: Failed to load data from dataSource', error);
76+
setReportRuntimeData([]);
77+
} finally {
78+
setDataLoading(false);
79+
}
80+
};
81+
82+
fetchDataFromSource();
83+
return;
84+
}
85+
86+
// If report has an objectName, fetch data from that object
87+
if (reportData.objectName) {
88+
const fetchDataFromObject = async () => {
89+
setDataLoading(true);
90+
try {
91+
const result = await dataSource.find(reportData.objectName, {
92+
$filter: reportData.filters,
93+
$orderby: reportData.sort,
94+
$top: reportData.limit || 100, // Default limit to avoid fetching too much data
95+
});
96+
97+
setReportRuntimeData(result.data || []);
98+
} catch (error) {
99+
console.error('ReportView: Failed to load data from objectName', error);
100+
setReportRuntimeData([]);
101+
} finally {
102+
setDataLoading(false);
103+
}
104+
};
105+
106+
fetchDataFromObject();
107+
return;
108+
}
109+
110+
// No data source configured
111+
setReportRuntimeData([]);
112+
}, [reportData, dataSource]);
113+
36114
if (loading) {
37115
return (
38116
<div className="h-full flex items-center justify-center p-8">
@@ -96,8 +174,10 @@ export function ReportView({ dataSource: _dataSource }: { dataSource?: any }) {
96174
const viewerSchema = {
97175
type: 'report-viewer',
98176
report: reportData, // The report definition
177+
data: reportRuntimeData, // Runtime data fetched from the data source
99178
showToolbar: true,
100-
allowExport: true
179+
allowExport: true,
180+
loading: dataLoading, // Loading state for data fetching
101181
};
102182

103183
return (

0 commit comments

Comments
 (0)