fix(studio): Route Code tab to CodeExporter instead of always rendering preview#1080
fix(studio): Route Code tab to CodeExporter instead of always rendering preview#1080
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ing preview Split default-plugin's single viewer (modes: ['preview', 'code']) into two separate viewer contributions: - json-inspector (preview mode) → MetadataInspector - code-exporter (code mode) → CodeExporter The CodeViewerComponent loads metadata via useClient().meta.getItem() and maps plural metadataType names to CodeExporter's singular ExportType. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/862dcad5-06e8-4973-a996-340dccc3789e Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes Studio’s metadata “Code” tab behavior by correctly routing code-mode viewing to CodeExporter (instead of always rendering the JSON preview inspector), improving the default fallback viewer wiring in the built-in Studio plugin.
Changes:
- Split the default wildcard viewer into two separate viewer contributions: preview (
json-inspector) vs code (code-exporter). - Added a
CodeViewerComponentthat loads metadata viauseClient().meta.getItem()and rendersCodeExporter. - Updated changelog to document the Studio Code tab fix.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| CHANGELOG.md | Documents the Studio Code tab fix and viewer split. |
| apps/studio/src/plugins/built-in/default-plugin.tsx | Splits viewer contributions by mode and introduces a code-mode viewer that renders CodeExporter. |
| metadataViewers: [ | ||
| { | ||
| id: 'json-inspector', | ||
| metadataTypes: ['*'], // Wildcard: matches all types | ||
| metadataTypes: ['*'], | ||
| label: 'JSON Inspector', | ||
| priority: -1, // Lowest priority — any plugin overrides this | ||
| modes: ['preview', 'code'], | ||
| priority: -1, | ||
| modes: ['preview'], | ||
| }, | ||
| { | ||
| id: 'code-exporter', | ||
| metadataTypes: ['*'], | ||
| label: 'Code Export', | ||
| priority: -1, | ||
| modes: ['code'], | ||
| }, |
There was a problem hiding this comment.
code-exporter is registered for metadataTypes: ['*'], but CodeExporter only supports a limited set of export types (object|view|flow|agent|app). For other Studio metadata types (e.g. actions, pages, dashboards, workflows, ragPipelines, roles, etc.), exportType falls back to 'object', which will generate misleading/incorrect TypeScript (wrong top-level stack key).
Consider either (a) restricting the code-exporter contribution to only the metadata types you can correctly map, or (b) detecting unsupported types in CodeViewerComponent and showing a clear “export not supported for this type” message / JSON-only export instead of defaulting to 'object'.
| const [definition, setDefinition] = useState<Record<string, unknown> | null>(data ?? null); | ||
| const [loading, setLoading] = useState(!data); | ||
|
|
||
| useEffect(() => { | ||
| // If data was passed directly, use it | ||
| if (data) { | ||
| setDefinition(data as Record<string, unknown>); | ||
| setLoading(false); | ||
| return; | ||
| } | ||
|
|
||
| let mounted = true; | ||
| setLoading(true); | ||
|
|
||
| async function load() { | ||
| try { | ||
| const result: any = await client.meta.getItem(metadataType, metadataName, packageId ? { packageId } : undefined); | ||
| if (mounted) { | ||
| setDefinition(result?.item || result); | ||
| } | ||
| } catch (err) { | ||
| console.error(`[CodeViewer] Failed to load ${metadataType}/${metadataName}:`, err); | ||
| } finally { | ||
| if (mounted) setLoading(false); | ||
| } | ||
| } | ||
| load(); | ||
| return () => { mounted = false; }; | ||
| }, [client, metadataType, metadataName, data, packageId]); | ||
|
|
||
| if (loading) { |
There was a problem hiding this comment.
CodeViewerComponent keeps definition state across metadata navigation. When metadataType/metadataName changes, the component can render once with the previous definition but the new metadataName (until the effect flips loading), which can briefly show incorrect exported code.
To avoid this, track which {metadataType, metadataName, packageId} the current definition belongs to (e.g., a loadedKey state) and render the loading/empty state whenever the props key doesn’t match, or clear definition synchronously when props change (so you never render with stale data).
| const [definition, setDefinition] = useState<Record<string, unknown> | null>(data ?? null); | |
| const [loading, setLoading] = useState(!data); | |
| useEffect(() => { | |
| // If data was passed directly, use it | |
| if (data) { | |
| setDefinition(data as Record<string, unknown>); | |
| setLoading(false); | |
| return; | |
| } | |
| let mounted = true; | |
| setLoading(true); | |
| async function load() { | |
| try { | |
| const result: any = await client.meta.getItem(metadataType, metadataName, packageId ? { packageId } : undefined); | |
| if (mounted) { | |
| setDefinition(result?.item || result); | |
| } | |
| } catch (err) { | |
| console.error(`[CodeViewer] Failed to load ${metadataType}/${metadataName}:`, err); | |
| } finally { | |
| if (mounted) setLoading(false); | |
| } | |
| } | |
| load(); | |
| return () => { mounted = false; }; | |
| }, [client, metadataType, metadataName, data, packageId]); | |
| if (loading) { | |
| const currentKey = `${metadataType}::${metadataName}::${packageId ?? ''}`; | |
| const [definition, setDefinition] = useState<Record<string, unknown> | null>(data ?? null); | |
| const [loadedKey, setLoadedKey] = useState<string | null>(data ? currentKey : null); | |
| const [loading, setLoading] = useState(!data); | |
| useEffect(() => { | |
| // If data was passed directly, bind it to the current metadata identity. | |
| if (data) { | |
| setDefinition(data as Record<string, unknown>); | |
| setLoadedKey(currentKey); | |
| setLoading(false); | |
| return; | |
| } | |
| let mounted = true; | |
| setLoading(true); | |
| setDefinition(null); | |
| setLoadedKey(null); | |
| async function load() { | |
| try { | |
| const result: any = await client.meta.getItem(metadataType, metadataName, packageId ? { packageId } : undefined); | |
| if (mounted) { | |
| setDefinition((result?.item || result) ?? null); | |
| setLoadedKey(currentKey); | |
| } | |
| } catch (err) { | |
| console.error(`[CodeViewer] Failed to load ${metadataType}/${metadataName}:`, err); | |
| if (mounted) { | |
| setDefinition(null); | |
| setLoadedKey(currentKey); | |
| } | |
| } finally { | |
| if (mounted) setLoading(false); | |
| } | |
| } | |
| load(); | |
| return () => { mounted = false; }; | |
| }, [client, metadataType, metadataName, data, packageId, currentKey]); | |
| if (loading || loadedKey !== currentKey) { |
The default-plugin registered a single
json-inspectorviewer claimingmodes: ['preview', 'code'], but the component unconditionally rendered<MetadataInspector>— themodeprop was ignored.Changes
default-plugin.tsx:json-inspector→modes: ['preview']→MetadataInspector(unchanged)code-exporter→modes: ['code']→CodeExporter(new)CodeViewerComponentloads metadata viauseClient().meta.getItem()(same pattern asMetadataInspector) and renders the existing<CodeExporter>with TypeScript/JSON toggle + copy-to-clipboardobjects,flows,agents, etc.) toCodeExporter's singularExportTypeNo changes to
plugin-host.tsxorCodeExporter.tsx— the infrastructure already resolved viewers by mode correctly; the default-plugin just wasn't wired up properly.