Uh oh!
There was an error while loading. Please reload this page.
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>
Uh oh!
There was an error while loading. Please reload this page.
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 previousdefinition but the newmetadataName (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(dataasRecord<string,unknown>); | |
| setLoading(false); | |
| return; | |
| } | |
| letmounted=true; | |
| setLoading(true); | |
| asyncfunctionload(){ | |
| try{ | |
| constresult: any=awaitclient.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){ | |
| constcurrentKey=`${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(dataasRecord<string,unknown>); | |
| setLoadedKey(currentKey); | |
| setLoading(false); | |
| return; | |
| } | |
| letmounted=true; | |
| setLoading(true); | |
| setDefinition(null); | |
| setLoadedKey(null); | |
| asyncfunctionload(){ | |
| try{ | |
| constresult: any=awaitclient.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.