Skip to content

Commit 38e6924

Browse files
Copilothotlong
andcommitted
fix: call metadata.refresh() after dashboard save, persist editSchema after panel close
- DashboardView: saveSchema now calls refresh() after successful save - DashboardView: previewSchema uses editSchema even after panel close (until metadata refreshes) - DashboardView: useEffect clears stale editSchema on metadata refresh while panel is closed - DashboardView: clear editSchema on dashboard navigation - DashboardDesignPage: saveSchema now calls refresh() after successful save - Add 5 new tests verifying metadata refresh after save operations Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent e4eb17d commit 38e6924

File tree

5 files changed

+111
-10
lines changed

5 files changed

+111
-10
lines changed

apps/console/src/__tests__/DashboardDesignInteraction.test.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
1414
import { DashboardView } from '../components/DashboardView';
1515

1616
// Track props passed to mocked components
17-
const { rendererCalls, dashboardConfigCalls, widgetConfigCalls } = vi.hoisted(() => ({
17+
const { rendererCalls, dashboardConfigCalls, widgetConfigCalls, mockRefresh } = vi.hoisted(() => ({
1818
rendererCalls: {
1919
designMode: false,
2020
selectedWidgetId: null as string | null,
@@ -32,6 +32,7 @@ const { rendererCalls, dashboardConfigCalls, widgetConfigCalls } = vi.hoisted(()
3232
onSave: null as ((config: Record<string, any>) => void) | null,
3333
onFieldChange: null as ((field: string, value: any) => void) | null,
3434
},
35+
mockRefresh: vi.fn().mockResolvedValue(undefined),
3536
}));
3637

3738
// Mock MetadataProvider with a dashboard
@@ -57,7 +58,7 @@ vi.mock('../context/MetadataProvider', () => ({
5758
pages: [],
5859
loading: false,
5960
error: null,
60-
refresh: vi.fn(),
61+
refresh: mockRefresh,
6162
}),
6263
}));
6364

@@ -132,6 +133,7 @@ vi.mock('sonner', () => ({
132133

133134
beforeEach(() => {
134135
mockUpdate.mockClear();
136+
mockRefresh.mockClear();
135137
rendererCalls.designMode = false;
136138
rendererCalls.selectedWidgetId = null;
137139
rendererCalls.onWidgetClick = null;
@@ -308,4 +310,21 @@ describe('Dashboard Design Mode — Inline Config Panel', () => {
308310
// Config panel should still show the widget (not reset or disappear)
309311
expect(screen.getByTestId('widget-config-panel')).toBeInTheDocument();
310312
});
313+
314+
it('should call metadata refresh after widget deletion (save)', async () => {
315+
await renderDashboardView();
316+
await openConfigPanel();
317+
318+
await act(async () => {
319+
fireEvent.click(screen.getByTestId('renderer-widget-w1'));
320+
});
321+
322+
mockRefresh.mockClear();
323+
await act(async () => {
324+
fireEvent.click(screen.getByTestId('widget-delete-button'));
325+
});
326+
327+
expect(mockUpdate).toHaveBeenCalled();
328+
expect(mockRefresh).toHaveBeenCalled();
329+
});
311330
});

apps/console/src/__tests__/DashboardDesignPage.test.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
1010
import { DashboardDesignPage } from '../pages/DashboardDesignPage';
1111

1212
// Mock MetadataProvider
13+
const { mockRefresh } = vi.hoisted(() => ({ mockRefresh: vi.fn().mockResolvedValue(undefined) }));
1314
vi.mock('../context/MetadataProvider', () => ({
1415
useMetadata: () => ({
1516
apps: [],
@@ -30,7 +31,7 @@ vi.mock('../context/MetadataProvider', () => ({
3031
pages: [],
3132
loading: false,
3233
error: null,
33-
refresh: vi.fn(),
34+
refresh: mockRefresh,
3435
}),
3536
}));
3637

@@ -118,4 +119,28 @@ describe('DashboardDesignPage', () => {
118119
// Should call dataSource.update with the dashboard schema
119120
expect(mockUpdate).toHaveBeenCalledWith('sys_dashboard', 'sales-dashboard', expect.objectContaining({ type: 'dashboard' }));
120121
});
122+
123+
it('should refresh metadata after save via onChange', async () => {
124+
renderWithRouter('sales-dashboard');
125+
mockRefresh.mockClear();
126+
127+
await act(async () => {
128+
fireEvent.click(screen.getByTestId('trigger-change'));
129+
});
130+
131+
expect(mockUpdate).toHaveBeenCalled();
132+
expect(mockRefresh).toHaveBeenCalled();
133+
});
134+
135+
it('should refresh metadata after Ctrl+S save', async () => {
136+
renderWithRouter('sales-dashboard');
137+
mockRefresh.mockClear();
138+
139+
await act(async () => {
140+
fireEvent.keyDown(window, { key: 's', ctrlKey: true });
141+
});
142+
143+
expect(mockUpdate).toHaveBeenCalled();
144+
expect(mockRefresh).toHaveBeenCalled();
145+
});
121146
});

apps/console/src/__tests__/DashboardViewSelection.test.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
1111
import { DashboardView } from '../components/DashboardView';
1212

1313
// Track the latest props passed to mocked components
14-
const { rendererCalls, dashboardConfigCalls, widgetConfigCalls } = vi.hoisted(() => ({
14+
const { rendererCalls, dashboardConfigCalls, widgetConfigCalls, mockRefresh } = vi.hoisted(() => ({
1515
rendererCalls: {
1616
designMode: false,
1717
selectedWidgetId: null as string | null,
@@ -31,6 +31,7 @@ const { rendererCalls, dashboardConfigCalls, widgetConfigCalls } = vi.hoisted(()
3131
onFieldChange: null as ((field: string, value: any) => void) | null,
3232
onClose: null as (() => void) | null,
3333
},
34+
mockRefresh: vi.fn().mockResolvedValue(undefined),
3435
}));
3536

3637
// Mock MetadataProvider with a dashboard
@@ -55,7 +56,7 @@ vi.mock('../context/MetadataProvider', () => ({
5556
pages: [],
5657
loading: false,
5758
error: null,
58-
refresh: vi.fn(),
59+
refresh: mockRefresh,
5960
}),
6061
}));
6162

@@ -136,6 +137,7 @@ vi.mock('sonner', () => ({
136137

137138
beforeEach(() => {
138139
mockUpdate.mockClear();
140+
mockRefresh.mockClear();
139141
rendererCalls.designMode = false;
140142
rendererCalls.selectedWidgetId = null;
141143
rendererCalls.onWidgetClick = null;
@@ -360,4 +362,43 @@ describe('DashboardView — Selection Sync Integration', () => {
360362
// Widget config panel should still be visible
361363
expect(screen.getByTestId('widget-config-panel')).toBeInTheDocument();
362364
});
365+
366+
it('should call metadata refresh after widget config save', async () => {
367+
await renderDashboardView();
368+
369+
await act(async () => {
370+
fireEvent.click(screen.getByTestId('dashboard-edit-button'));
371+
});
372+
await act(async () => {
373+
fireEvent.click(screen.getByTestId('renderer-widget-w1'));
374+
});
375+
376+
mockRefresh.mockClear();
377+
await act(async () => {
378+
fireEvent.click(screen.getByTestId('widget-config-save'));
379+
});
380+
381+
// Backend save should trigger metadata refresh
382+
expect(mockUpdate).toHaveBeenCalled();
383+
expect(mockRefresh).toHaveBeenCalled();
384+
});
385+
386+
it('should call metadata refresh after widget deletion', async () => {
387+
await renderDashboardView();
388+
389+
await act(async () => {
390+
fireEvent.click(screen.getByTestId('dashboard-edit-button'));
391+
});
392+
await act(async () => {
393+
fireEvent.click(screen.getByTestId('renderer-widget-w1'));
394+
});
395+
396+
mockRefresh.mockClear();
397+
await act(async () => {
398+
fireEvent.click(screen.getByTestId('widget-delete-button'));
399+
});
400+
401+
expect(mockUpdate).toHaveBeenCalled();
402+
expect(mockRefresh).toHaveBeenCalled();
403+
});
363404
});

apps/console/src/components/DashboardView.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,27 +138,41 @@ export function DashboardView({ dataSource }: { dataSource?: any }) {
138138

139139
useEffect(() => {
140140
setIsLoading(true);
141+
setEditSchema(null);
142+
setConfigPanelOpen(false);
143+
setSelectedWidgetId(null);
141144
queueMicrotask(() => setIsLoading(false));
142145
}, [dashboardName]);
143146

144-
const { dashboards, objects: metadataObjects } = useMetadata();
147+
const { dashboards, objects: metadataObjects, refresh } = useMetadata();
145148
const dashboard = dashboards?.find((d: any) => d.name === dashboardName);
146149

147150
// Local schema state for live preview — initialized from metadata
148151
const [editSchema, setEditSchema] = useState<DashboardSchema | null>(null);
149152

153+
// When metadata refreshes (dashboard reference changes), discard stale
154+
// editSchema if the config panel is already closed.
155+
useEffect(() => {
156+
if (!configPanelOpen) {
157+
setEditSchema(null);
158+
}
159+
// eslint-disable-next-line react-hooks/exhaustive-deps
160+
}, [dashboard]);
161+
150162
// ---- Save helper --------------------------------------------------------
151163
const saveSchema = useCallback(
152164
async (schema: DashboardSchema) => {
153165
try {
154166
if (adapter) {
155167
await adapter.update('sys_dashboard', dashboardName!, schema);
168+
// Refresh metadata cache so closing the config panel shows saved data
169+
refresh().catch(() => {});
156170
}
157171
} catch (err) {
158172
console.warn('[DashboardView] Auto-save failed:', err);
159173
}
160174
},
161-
[adapter, dashboardName],
175+
[adapter, dashboardName, refresh],
162176
);
163177

164178
// ---- Open / close config panel ------------------------------------------
@@ -360,7 +374,7 @@ export function DashboardView({ dataSource }: { dataSource?: any }) {
360374
);
361375
}
362376

363-
const previewSchema = configPanelOpen && editSchema ? editSchema : dashboard;
377+
const previewSchema = editSchema || dashboard;
364378

365379
return (
366380
<div className="flex flex-col h-full overflow-hidden bg-background">

apps/console/src/pages/DashboardDesignPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function DashboardDesignPage() {
1919
const navigate = useNavigate();
2020
const { dashboardName } = useParams<{ dashboardName: string }>();
2121
const dataSource = useAdapter();
22-
const { dashboards } = useMetadata();
22+
const { dashboards, refresh } = useMetadata();
2323

2424
const dashboard = dashboards?.find((d: any) => d.name === dashboardName);
2525

@@ -41,14 +41,16 @@ export function DashboardDesignPage() {
4141
try {
4242
if (dataSource) {
4343
await dataSource.update('sys_dashboard', dashboardName!, toSave);
44+
// Refresh metadata cache so other pages see saved changes
45+
refresh().catch(() => {});
4446
return true;
4547
}
4648
} catch {
4749
// Save errors are non-blocking; user can retry via export
4850
}
4951
return false;
5052
},
51-
[dataSource, dashboardName],
53+
[dataSource, dashboardName, refresh],
5254
);
5355

5456
const handleChange = useCallback(

0 commit comments

Comments
 (0)