Skip to content

Commit 69e66a4

Browse files
authored
Merge pull request #855 from objectstack-ai/copilot/fix-dashboard-refresh-issue
2 parents 7a455fb + b187b53 commit 69e66a4

File tree

6 files changed

+121
-12
lines changed

6 files changed

+121
-12
lines changed

ROADMAP.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
1919
1. ~~**AppShell** — No dynamic navigation renderer from spec JSON (last P0 blocker)~~ ✅ Complete
2020
2. **Designer Interaction** — ViewDesigner and DataModelDesigner have undo/redo, field type selectors, inline editing, Ctrl+S save, column drag-to-reorder with dnd-kit ✅
2121
3. **View Config Live Preview Sync** — Config panel changes sync in real-time for Grid, but `showSort`/`showSearch`/`showFilters`/`striped`/`bordered` not yet propagated to Kanban/Calendar/Timeline/Gallery/Map/Gantt (see P1.8.1)
22-
4. **Dashboard Config Panel** — Airtable-style right-side configuration panel for dashboards (data source, layout, widget properties, sub-editors, type definitions). Widget config live preview sync and scatter chart type switch ✅ fixed (P1.10 Phase 10).
22+
4. **Dashboard Config Panel** — Airtable-style right-side configuration panel for dashboards (data source, layout, widget properties, sub-editors, type definitions). Widget config live preview sync and scatter chart type switch ✅ fixed (P1.10 Phase 10). Dashboard save/refresh metadata sync ✅ fixed (P1.10 Phase 11).
2323
5. **Console Advanced Polish** — Remaining upgrades for forms, import/export, automation, comments
2424
6. **PWA Sync** — Background sync is simulated only
2525

@@ -435,6 +435,14 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
435435
- [x] Support data-table construction from `widget.object` when no data provider exists (table widgets created via config panel)
436436
- [x] Add 7 new Vitest tests: scatter chart (2), widget-level field fallbacks (2), object-chart from widget fields, data-table from widget.object, DashboardWithConfig live preview
437437

438+
**Phase 11 — Dashboard Save/Refresh Metadata Sync:**
439+
- [x] Fix: `saveSchema` in `DashboardView` did not call `metadata.refresh()` after PATCH — closing config panel showed stale data from cached metadata
440+
- [x] Fix: `previewSchema` only used `editSchema` when `configPanelOpen=true` — changed to `editSchema || dashboard` so edits remain visible after panel close until metadata refreshes
441+
- [x] Add `useEffect` to clear stale `editSchema` when metadata refreshes while config panel is closed (seamless transition)
442+
- [x] Clear `editSchema` and config panel state on dashboard navigation (`dashboardName` change)
443+
- [x] Fix: `DashboardDesignPage.saveSchema` did not call `metadata.refresh()` — other pages saw stale dashboard data after save
444+
- [x] Add 5 new Vitest tests: metadata refresh after widget save (2), metadata refresh after widget delete (2), metadata refresh after DashboardDesignPage save (1)
445+
438446
### P1.11 Console — Schema-Driven View Config Panel Migration
439447

440448
> Migrated the Console ViewConfigPanel from imperative implementation (~1655 lines) to Schema-Driven architecture using `ConfigPanelRenderer` + `useConfigDraft` + `ConfigPanelSchema`, reducing to ~170 lines declarative wrapper + schema factory.
@@ -1004,6 +1012,6 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
10041012

10051013
---
10061014

1007-
**Roadmap Status:** 🎯 Active — AppShell · Designer Interaction · View Config Live Preview Sync (P1.8.1) · Dashboard Config Panel (widget live preview & scatter type switch ✅ fixed) · Schema-Driven View Config Panel ✅ · Right-Side Visual Editor Drawer ✅ · Airtable UX Parity
1015+
**Roadmap Status:** 🎯 Active — AppShell · Designer Interaction · View Config Live Preview Sync (P1.8.1) · Dashboard Config Panel (widget live preview & scatter type switch ✅ fixed, save/refresh metadata sync ✅ fixed) · Schema-Driven View Config Panel ✅ · Right-Side Visual Editor Drawer ✅ · Airtable UX Parity
10081016
**Next Review:** March 15, 2026
10091017
**Contact:** hello@objectui.org | https://github.com/objectstack-ai/objectui

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)