diff --git a/ROADMAP.md b/ROADMAP.md
index 771ba20e1..8474cb72c 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -259,6 +259,8 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
7. ~~**Export toggle broken:** ViewConfigPanel writes `allowExport: boolean` but ListView checks `exportOptions` object~~ → Export now checks both `exportOptions && allowExport !== false`; Console clears `exportOptions` when `allowExport === false` (Issue #719)
8. ~~**`hasExport` logic bug:** `draft.allowExport !== false` was always true when undefined~~ → Fixed to `draft.allowExport === true || draft.exportOptions != null` (Issue #719)
9. **No per-view-type integration tests:** Pending — tests verify config reaches `fullSchema`, but per-renderer integration tests still needed
+10. ~~**`key={refreshKey}` on PluginObjectView:** Console wrapped PluginObjectView with `key={refreshKey}`, which only changed on save/create, preventing live preview of config changes~~ → Removed `key={refreshKey}`; props changes now flow naturally without remounting (Issue #784)
+11. ~~**Navigation overlay not consuming `activeView.navigation`:** Detail overlay only read `objectDef.navigation`, ignoring view-level navigation config~~ → Navigation now uses priority: `activeView.navigation > objectDef.navigation > default drawer` (Issue #784)
**Phase 1 — Grid/Table View (baseline, already complete):**
- [x] `gridSchema` includes `striped`/`bordered` from `activeView`
diff --git a/apps/console/src/__tests__/ObjectView.test.tsx b/apps/console/src/__tests__/ObjectView.test.tsx
index 6321dc9a4..80edce5bf 100644
--- a/apps/console/src/__tests__/ObjectView.test.tsx
+++ b/apps/console/src/__tests__/ObjectView.test.tsx
@@ -17,6 +17,27 @@ vi.mock('@object-ui/plugin-calendar', () => ({
ObjectCalendar: (props: any) =>
Calendar View: {props.schema.dateField}
}));
+// Mock ListView to a simple component that renders schema properties as test IDs
+// This isolates the config panel → view data flow from ListView's internal async effects
+vi.mock('@object-ui/plugin-list', () => ({
+ ListView: (props: any) => {
+ const viewType = props.schema?.viewType || 'grid';
+ return (
+
+ {viewType === 'grid' &&
Grid View: {props.schema?.objectName}
}
+ {viewType === 'kanban' &&
Kanban View: {props.schema?.options?.kanban?.groupField || props.schema?.groupBy}
}
+ {viewType === 'calendar' &&
Calendar View: {props.schema?.options?.calendar?.startDateField || props.schema?.startDateField}
}
+ {props.schema?.showRecordCount &&
showRecordCount
}
+ {props.schema?.allowPrinting &&
allowPrinting
}
+ {props.schema?.navigation?.mode &&
{props.schema.navigation.mode}
}
+ {props.schema?.selection?.type &&
{props.schema.selection.type}
}
+ {props.schema?.addRecord?.enabled &&
addRecord
}
+ {props.schema?.addRecordViaForm &&
addRecordViaForm
}
+
+ );
+ },
+}));
+
vi.mock('@object-ui/components', async (importOriginal) => {
const React = await import('react');
const MockTabsContext = React.createContext({ onValueChange: ( _v: any) => {} });
@@ -543,4 +564,200 @@ describe('ObjectView Component', () => {
expect(breadcrumbItems.length).toBeGreaterThanOrEqual(1);
});
});
+
+ it('does not remount PluginObjectView on config panel changes (no key={refreshKey})', async () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ // The grid should be rendered initially
+ expect(screen.getByTestId('object-grid')).toBeInTheDocument();
+
+ // Open config panel
+ fireEvent.click(screen.getByTitle('console.objectView.designTools'));
+ fireEvent.click(screen.getByText('console.objectView.editView'));
+
+ // Make a change
+ const titleInput = await screen.findByDisplayValue('All Opportunities');
+ fireEvent.change(titleInput, { target: { value: 'Changed Live' } });
+
+ // The breadcrumb updates immediately (live preview) — this verifies that
+ // viewDraft → activeView data flow propagates config changes without save.
+ await vi.waitFor(() => {
+ expect(screen.getByText('Changed Live')).toBeInTheDocument();
+ });
+
+ // Grid persists after config change (no remount)
+ expect(screen.getByTestId('object-grid')).toBeInTheDocument();
+ });
+
+ it('propagates showRecordCount toggle to ListView schema in real-time', async () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ // showRecordCount should not be set initially (default is not explicitly true)
+ expect(screen.queryByTestId('schema-showRecordCount')).not.toBeInTheDocument();
+
+ // Open config panel
+ fireEvent.click(screen.getByTitle('console.objectView.designTools'));
+ fireEvent.click(screen.getByText('console.objectView.editView'));
+
+ // Toggle showRecordCount on
+ const recordCountSwitch = screen.getByTestId('toggle-showRecordCount');
+ fireEvent.click(recordCountSwitch);
+
+ // Verify the schema property propagated to ListView immediately (live preview)
+ await vi.waitFor(() => {
+ expect(screen.getByTestId('schema-showRecordCount')).toBeInTheDocument();
+ });
+ });
+
+ it('propagates allowPrinting toggle to ListView schema in real-time', async () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ // allowPrinting should not be set initially
+ expect(screen.queryByTestId('schema-allowPrinting')).not.toBeInTheDocument();
+
+ // Open config panel
+ fireEvent.click(screen.getByTitle('console.objectView.designTools'));
+ fireEvent.click(screen.getByText('console.objectView.editView'));
+
+ // Toggle allowPrinting on
+ const printSwitch = screen.getByTestId('toggle-allowPrinting');
+ fireEvent.click(printSwitch);
+
+ // Verify the schema property propagated to ListView immediately (live preview)
+ await vi.waitFor(() => {
+ expect(screen.getByTestId('schema-allowPrinting')).toBeInTheDocument();
+ });
+ });
+
+ it('propagates multiple config changes without requiring save', async () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ // Open config panel
+ fireEvent.click(screen.getByTitle('console.objectView.designTools'));
+ fireEvent.click(screen.getByText('console.objectView.editView'));
+
+ // Toggle showSearch off
+ const searchSwitch = screen.getByTestId('toggle-showSearch');
+ fireEvent.click(searchSwitch);
+
+ // Toggle showSort off
+ const sortSwitch = screen.getByTestId('toggle-showSort');
+ fireEvent.click(sortSwitch);
+
+ // Both should reflect changes immediately without save
+ await vi.waitFor(() => {
+ expect(screen.getByTestId('toggle-showSearch').getAttribute('aria-checked')).toBe('false');
+ expect(screen.getByTestId('toggle-showSort').getAttribute('aria-checked')).toBe('false');
+ });
+
+ // The grid should still be rendered (live preview, no remount)
+ expect(screen.getByTestId('object-grid')).toBeInTheDocument();
+ });
+
+ it('uses activeView.navigation for detail overlay with priority over objectDef', () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ const objectsWithNav = [
+ {
+ ...mockObjects[0],
+ navigation: { mode: 'drawer' as const },
+ listViews: {
+ all: {
+ label: 'All Opportunities',
+ type: 'grid',
+ columns: ['name', 'stage'],
+ navigation: { mode: 'modal' as const },
+ },
+ pipeline: { label: 'Pipeline', type: 'kanban', kanban: { groupField: 'stage' }, columns: ['name'] }
+ }
+ }
+ ];
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ // Render the component — activeView.navigation should override objectDef.navigation
+ render();
+
+ // The component should render without errors and ListView should receive
+ // the view-level navigation config (modal) instead of object-level (drawer)
+ expect(screen.getByTestId('object-grid')).toBeInTheDocument();
+ expect(screen.getByTestId('schema-navigation-mode')).toHaveTextContent('modal');
+ });
+
+ it('propagates selection mode change to ListView schema in real-time', async () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ // Open config panel
+ fireEvent.click(screen.getByTitle('console.objectView.designTools'));
+ fireEvent.click(screen.getByText('console.objectView.editView'));
+
+ // Change selection mode to 'single'
+ const selectionSelect = screen.getByTestId('select-selection-type');
+ fireEvent.change(selectionSelect, { target: { value: 'single' } });
+
+ // Verify the selection type propagated to ListView immediately (live preview)
+ await vi.waitFor(() => {
+ expect(screen.getByTestId('schema-selection-type')).toHaveTextContent('single');
+ });
+ });
+
+ it('propagates addRecord toggle to ListView schema in real-time', async () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ // addRecord should not be enabled initially
+ expect(screen.queryByTestId('schema-addRecord-enabled')).not.toBeInTheDocument();
+
+ // Open config panel
+ fireEvent.click(screen.getByTitle('console.objectView.designTools'));
+ fireEvent.click(screen.getByText('console.objectView.editView'));
+
+ // Toggle addRecord on
+ const addRecordSwitch = screen.getByTestId('toggle-addRecord-enabled');
+ fireEvent.click(addRecordSwitch);
+
+ // Verify addRecord and addRecordViaForm propagated to ListView immediately
+ await vi.waitFor(() => {
+ expect(screen.getByTestId('schema-addRecord-enabled')).toBeInTheDocument();
+ expect(screen.getByTestId('schema-addRecordViaForm')).toBeInTheDocument();
+ });
+ });
+
+ it('propagates navigation mode change from config panel to ListView schema in real-time', async () => {
+ mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
+ mockUseParams.mockReturnValue({ objectName: 'opportunity' });
+
+ render();
+
+ // navigation mode should not be set initially (no explicit mode on default view)
+ expect(screen.queryByTestId('schema-navigation-mode')).not.toBeInTheDocument();
+
+ // Open config panel
+ fireEvent.click(screen.getByTitle('console.objectView.designTools'));
+ fireEvent.click(screen.getByText('console.objectView.editView'));
+
+ // Change navigation mode to 'modal'
+ const navSelect = screen.getByTestId('select-navigation-mode');
+ fireEvent.change(navSelect, { target: { value: 'modal' } });
+
+ // Verify navigation mode propagated to ListView schema immediately (live preview)
+ await vi.waitFor(() => {
+ expect(screen.getByTestId('schema-navigation-mode')).toHaveTextContent('modal');
+ });
+ });
});
diff --git a/apps/console/src/components/ObjectView.tsx b/apps/console/src/components/ObjectView.tsx
index 88d71275a..4f8e3bc46 100644
--- a/apps/console/src/components/ObjectView.tsx
+++ b/apps/console/src/components/ObjectView.tsx
@@ -230,7 +230,8 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
}, [dataSource, objectDef.name, refreshKey]);
// Navigation overlay for record detail (supports drawer/modal/split/popover via config)
- const detailNavigation: ViewNavigationConfig = objectDef.navigation ?? { mode: 'drawer' };
+ // Priority: activeView.navigation > objectDef.navigation > default drawer
+ const detailNavigation: ViewNavigationConfig = activeView?.navigation ?? objectDef.navigation ?? { mode: 'drawer' };
const drawerRecordId = searchParams.get('recordId');
const navOverlay = useNavigationOverlay({
navigation: detailNavigation,
@@ -573,7 +574,6 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {