Skip to content

Commit 52f78d9

Browse files
xuyushun441-sysos-zhuangclaude
authored
fix(studio): Preview tab renders interface pages via InterfaceListPage (#1691)
The metadata page editor's Preview tab rendered every page through the generic SchemaRenderer. For ADR-0047 interface pages (config-driven, `regions: []`, list generated from `interfaceConfig`) that produced only a bare list shell — no source binding, no user-filter chips, no data — so authors could not see their interfaceConfig (source view, userFilters, visualizations) reflected in the preview. PagePreview now mirrors the runtime PageView: when the draft is an interface page (`interfaceConfig.source`) and not in design mode, it renders InterfaceListPage with the live draft — exactly what end users see. Design mode still shows the canvas (the 'configured in Properties' hint). The interface branch sits AFTER all hooks (Rules of Hooks). Browser-verified on the showcase Task Workbench: the Preview tab now shows the status/priority filter dropdowns + the bound data table (10 records), matching the runtime page; previously empty. Locked by PagePreview.test.tsx (interface→InterfaceListPage, design→canvas, region page→SchemaRenderer). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e164c92 commit 52f78d9

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect, vi, afterEach } from 'vitest';
4+
import { render, screen, cleanup } from '@testing-library/react';
5+
6+
// Mock the heavy runtime/canvas children so the test isolates PagePreview's
7+
// routing decision (which child it picks), not their internals.
8+
vi.mock('../../InterfaceListPage', () => ({
9+
InterfaceListPage: () => <div data-testid="mock-interface-list" />,
10+
}));
11+
vi.mock('@object-ui/react', () => ({
12+
SchemaRenderer: () => <div data-testid="mock-schema-renderer" />,
13+
}));
14+
vi.mock('./PageBlockCanvas', () => ({
15+
PageBlockCanvas: () => <div data-testid="mock-page-canvas" />,
16+
}));
17+
18+
import { PagePreview } from './PagePreview';
19+
20+
afterEach(cleanup);
21+
22+
const interfaceDraft = {
23+
name: 'wb',
24+
type: 'list',
25+
regions: [],
26+
interfaceConfig: { source: 'task', userFilters: { element: 'dropdown', fields: [{ field: 'status' }] } },
27+
};
28+
const regionDraft = {
29+
name: 'home',
30+
type: 'home',
31+
regions: [{ name: 'main', components: [{ type: 'container' }] }],
32+
};
33+
34+
describe('PagePreview — interface-page routing (ADR-0047)', () => {
35+
it('renders the runtime InterfaceListPage for an interface page in preview mode', () => {
36+
// preview mode = no onSelectionChange (not editing the canvas)
37+
render(<PagePreview draft={interfaceDraft} />);
38+
expect(screen.getByTestId('mock-interface-list')).toBeInTheDocument();
39+
expect(screen.queryByTestId('mock-schema-renderer')).not.toBeInTheDocument();
40+
});
41+
42+
it('does NOT use InterfaceListPage in design mode (keeps the canvas hint)', () => {
43+
render(
44+
<PagePreview
45+
draft={interfaceDraft}
46+
editing
47+
onSelectionChange={() => {}}
48+
onPatch={() => {}}
49+
/>,
50+
);
51+
expect(screen.queryByTestId('mock-interface-list')).not.toBeInTheDocument();
52+
expect(screen.getByTestId('mock-page-canvas')).toBeInTheDocument();
53+
});
54+
55+
it('renders the generic SchemaRenderer for a region-composed page (not an interface page)', () => {
56+
render(<PagePreview draft={regionDraft} />);
57+
expect(screen.queryByTestId('mock-interface-list')).not.toBeInTheDocument();
58+
expect(screen.getByTestId('mock-schema-renderer')).toBeInTheDocument();
59+
});
60+
});

packages/app-shell/src/views/metadata-admin/previews/PagePreview.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { MetadataPreviewProps } from '../preview-registry';
1515
import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
1616
import { OutlineStrip } from './OutlineStrip';
1717
import { PageBlockCanvas } from './PageBlockCanvas';
18+
import { InterfaceListPage } from '../../InterfaceListPage';
1819
import { t as tr } from '../i18n';
1920

2021
interface Block { type?: string; id?: string; children?: Block[]; [k: string]: unknown }
@@ -32,6 +33,13 @@ export function PagePreview({ draft, editing, selection, onSelectionChange, onPa
3233
const canEdit = designMode && !!onPatch;
3334
const selectedId = selection && selection.kind === 'block' ? selection.id : null;
3435

36+
// ADR-0047 interface pages are config-driven, not region-composed. The
37+
// runtime (PageView) renders them via InterfaceListPage; the generic
38+
// SchemaRenderer fallback would only produce a bare list shell with no
39+
// source binding or user filters. Computed here, consumed after all hooks
40+
// (the early return must not sit above later hooks — Rules of Hooks).
41+
const isInterfacePage = !!(draft as { interfaceConfig?: { source?: string } })?.interfaceConfig?.source;
42+
3543
// Pages may use either of two canonical shapes:
3644
// 1. `regions: [{ name, components: [...] }]` (ObjectStack spec, used by seeded pages)
3745
// 2. `children: [...]` (raw SDUI tree shape)
@@ -89,6 +97,20 @@ export function PagePreview({ draft, editing, selection, onSelectionChange, onPa
8997
onSelectionChange?.({ kind: 'block', id: `children[${next.length - 1}]`, label: newBlock.type });
9098
}, [canEdit, draft, onPatch, onSelectionChange, shape]);
9199

100+
// Interface page in preview mode → mirror the runtime (InterfaceListPage)
101+
// so the Preview tab shows the real source view + user filters + data,
102+
// live from the edited draft. Design mode falls through to the canvas,
103+
// which shows the "configured in Properties" hint instead.
104+
if (isInterfacePage && !designMode) {
105+
return (
106+
<PreviewShell hint="page · interface">
107+
<PreviewErrorBoundary fallbackHint="The interface page references a source object/view that isn't available.">
108+
<InterfaceListPage page={draft as Record<string, unknown>} />
109+
</PreviewErrorBoundary>
110+
</PreviewShell>
111+
);
112+
}
113+
92114
// Empty draft → no preview; but if we're in design mode show the
93115
// canvas so users can author from scratch.
94116
if (!schema || Object.keys(schema).length <= 1) {

0 commit comments

Comments
 (0)