-
Notifications
You must be signed in to change notification settings - Fork 1
fix(studio): Route Code tab to CodeExporter instead of always rendering preview #1080
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,44 +3,130 @@ | |
| /** | ||
| * Built-in Plugin: Default Metadata Inspector | ||
| * | ||
| * Provides a JSON tree viewer as the fallback for any metadata type | ||
| * that doesn't have a specialized plugin. This is the "catch-all" viewer. | ||
| * Provides two fallback viewers for any metadata type: | ||
| * - **json-inspector** (preview mode): JSON tree viewer via MetadataInspector | ||
| * - **code-exporter** (code mode): Exportable TypeScript/JSON via CodeExporter | ||
| * | ||
| * Priority is set to -1 so any type-specific plugin will take precedence. | ||
| */ | ||
|
|
||
| import { useState, useEffect } from 'react'; | ||
| import { defineStudioPlugin } from '@objectstack/spec/studio'; | ||
| import { useClient } from '@objectstack/client-react'; | ||
| import { MetadataInspector } from '@/components/MetadataInspector'; | ||
| import { CodeExporter } from '@/components/CodeExporter'; | ||
| import type { CodeExporterProps } from '@/components/CodeExporter'; | ||
| import type { StudioPlugin, MetadataViewerProps } from '../types'; | ||
|
|
||
| // ─── Viewer Component (adapts MetadataInspector to plugin interface) ─ | ||
| // ─── Helpers ───────────────────────────────────────────────────────── | ||
|
|
||
| function DefaultViewerComponent({ metadataType, metadataName, packageId }: MetadataViewerProps) { | ||
| /** Map Studio metadataType (often plural) to CodeExporter's ExportType (singular). */ | ||
| const METADATA_TO_EXPORT_TYPE: Record<string, CodeExporterProps['type']> = { | ||
| object: 'object', | ||
| objects: 'object', | ||
| view: 'view', | ||
| views: 'view', | ||
| flow: 'flow', | ||
| flows: 'flow', | ||
| agent: 'agent', | ||
| agents: 'agent', | ||
| app: 'app', | ||
| apps: 'app', | ||
| }; | ||
|
|
||
| // ─── Preview Viewer (JSON Inspector) ───────────────────────────────── | ||
|
|
||
| function PreviewViewerComponent({ metadataType, metadataName, packageId }: MetadataViewerProps) { | ||
| return <MetadataInspector metaType={metadataType} metaName={metadataName} packageId={packageId} />; | ||
| } | ||
|
|
||
| // ─── Code Viewer (Code Exporter) ───────────────────────────────────── | ||
|
|
||
| function CodeViewerComponent({ metadataType, metadataName, data, packageId }: MetadataViewerProps) { | ||
| const client = useClient(); | ||
| 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) { | ||
| return ( | ||
| <div className="p-4 text-sm text-muted-foreground">Loading definition…</div> | ||
| ); | ||
| } | ||
|
|
||
| if (!definition) { | ||
| return ( | ||
| <div className="p-4 text-sm text-muted-foreground"> | ||
| Definition not found: <code className="font-mono">{metadataName}</code> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const exportType = METADATA_TO_EXPORT_TYPE[metadataType] ?? 'object'; | ||
|
|
||
| return ( | ||
| <div className="p-4"> | ||
| <CodeExporter type={exportType} definition={definition} name={metadataName} /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // ─── Plugin Definition ─────────────────────────────────────────────── | ||
|
|
||
| export const defaultInspectorPlugin: StudioPlugin = { | ||
| manifest: defineStudioPlugin({ | ||
| id: 'objectstack.default-inspector', | ||
| name: 'Default Metadata Inspector', | ||
| version: '1.0.0', | ||
| description: 'JSON tree viewer for any metadata type. Fallback when no specialized viewer is available.', | ||
| description: 'JSON tree viewer and code exporter for any metadata type. Fallback when no specialized viewer is available.', | ||
| contributes: { | ||
| 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'], | ||
| }, | ||
|
Comment on lines
109
to
123
|
||
| ], | ||
| }, | ||
| }), | ||
|
|
||
| activate(api) { | ||
| api.registerViewer('json-inspector', DefaultViewerComponent); | ||
| api.registerViewer('json-inspector', PreviewViewerComponent); | ||
| api.registerViewer('code-exporter', CodeViewerComponent); | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CodeViewerComponentkeepsdefinitionstate across metadata navigation. WhenmetadataType/metadataNamechanges, the component can render once with the previousdefinitionbut the newmetadataName(until the effect flipsloading), which can briefly show incorrect exported code.To avoid this, track which
{metadataType, metadataName, packageId}the currentdefinitionbelongs to (e.g., aloadedKeystate) and render the loading/empty state whenever the props key doesn’t match, or cleardefinitionsynchronously when props change (so you never render with stale data).