diff --git a/frontend-react/src/App.tsx b/frontend-react/src/App.tsx index ef54c8b..88d33e0 100644 --- a/frontend-react/src/App.tsx +++ b/frontend-react/src/App.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { analyze, autoLayout, compile, tidyLayout } from "./api/client"; import { CanvasArea } from "./components/CanvasArea"; import { LeftPanel } from "./components/LeftPanel"; @@ -40,6 +40,23 @@ function toCompileFromAnalyze(result: Awaited>): Reco }; } +function sortKeysDeep(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((item) => sortKeysDeep(item)); + } + if (value && typeof value === "object") { + const record = value as Record; + const sorted = Object.keys(record) + .sort((a, b) => a.localeCompare(b)) + .reduce>((acc, key) => { + acc[key] = sortKeysDeep(record[key]); + return acc; + }, {}); + return sorted; + } + return value; +} + export function App() { const store = useMemo(() => createSchemetaStore({ model: SAMPLE_PAYLOAD }), []); const actionsApi = useSchemetaActions(store); @@ -48,6 +65,13 @@ export function App() { const compileResult = useSchemetaSelector(store, (state) => state.compileResult); const lifecycle = useSchemetaSelector(store, (state) => state.lifecycle); const jsonError = useSchemetaSelector(store, (state) => state.uiFlags.jsonError); + const [jsonEditorText, setJsonEditorText] = useState(() => JSON.stringify(SAMPLE_PAYLOAD, null, 2)); + + useEffect(() => { + if (model) { + setJsonEditorText(JSON.stringify(model, null, 2)); + } + }, [model]); const lastApiVersion = useSchemetaSelector(store, (state) => { const apiVersion = state.compileResult && typeof state.compileResult.api_version === "string" ? state.compileResult.api_version : null; return apiVersion ?? "-"; @@ -149,6 +173,71 @@ export function App() { } } + function applyJsonFromEditor() { + const parsed = actionsApi.applyJsonText(jsonEditorText); + if (!parsed.ok) { + actionsApi.failCompile("apply-json", parsed.error); + return; + } + actionsApi.completeCompile("apply-json"); + } + + async function validateJsonEditor() { + const parsed = actionsApi.applyJsonText(jsonEditorText); + if (!parsed.ok) { + actionsApi.failCompile("validate-json", parsed.error); + return; + } + + const modelCandidate = store.getState().model; + if (!modelCandidate) { + actionsApi.failCompile("validate-json", "No model loaded"); + return; + } + + actionsApi.beginCompile("validate-json"); + try { + const analyzed = await analyze(modelCandidate); + actionsApi.setCompileResult(toCompileFromAnalyze(analyzed)); + actionsApi.completeCompile("validate-json"); + } catch (error) { + actionsApi.failCompile("validate-json", error); + } + } + + function formatJsonEditor() { + const parsed = actionsApi.applyJsonText(jsonEditorText); + if (!parsed.ok) { + actionsApi.failCompile("format-json", parsed.error); + return; + } + const next = store.getState().model; + if (next) { + setJsonEditorText(JSON.stringify(next, null, 2)); + } + actionsApi.completeCompile("format-json"); + } + + function sortJsonEditorKeys() { + try { + const parsed = JSON.parse(jsonEditorText); + const sorted = sortKeysDeep(parsed); + setJsonEditorText(JSON.stringify(sorted, null, 2)); + actionsApi.completeCompile("sort-json"); + } catch (error) { + actionsApi.failCompile("sort-json", error); + } + } + + async function copyReproJson() { + try { + await navigator.clipboard.writeText(jsonEditorText); + actionsApi.completeCompile("copy-repro"); + } catch { + actionsApi.failCompile("copy-repro", "Clipboard write failed"); + } + } + return (
- + { + void validateJsonEditor(); + }} + onFormatJson={formatJsonEditor} + onSortJson={sortJsonEditorKeys} + onCopyRepro={() => { + void copyReproJson(); + }} + />
); diff --git a/frontend-react/src/components/RightInspector.tsx b/frontend-react/src/components/RightInspector.tsx index 878c3fc..e31b13d 100644 --- a/frontend-react/src/components/RightInspector.tsx +++ b/frontend-react/src/components/RightInspector.tsx @@ -4,6 +4,14 @@ type RightInspectorProps = { model: SchemetaModel | null; selection: SelectionSlice; compileResult: unknown | null; + jsonText: string; + jsonError: string | null; + onJsonTextChange: (value: string) => void; + onApplyJson: () => void; + onValidateJson: () => void; + onFormatJson: () => void; + onSortJson: () => void; + onCopyRepro: () => void; }; function asRecord(value: unknown): Record | null { @@ -33,7 +41,19 @@ function toSnippet(value: unknown, fallback: string): string { } } -export function RightInspector({ model, selection, compileResult }: RightInspectorProps) { +export function RightInspector({ + model, + selection, + compileResult, + jsonText, + jsonError, + onJsonTextChange, + onApplyJson, + onValidateJson, + onFormatJson, + onSortJson, + onCopyRepro +}: RightInspectorProps) { const selectedRef = selection.selectedRefs[0] ?? null; const selectedInstance = selectedRef ? (model?.instances ?? []).find((instance) => instance.ref === selectedRef) : null; @@ -66,6 +86,35 @@ export function RightInspector({ model, selection, compileResult }: RightInspect

Layout Metrics

{toSnippet(layoutMetrics, "No layout metrics available yet.")}
+ +

JSON Editor

+
+ + + + + +
+