Sprint 6: add React JSON power-tool editor workflow
Some checks are pending
CI / test (push) Waiting to run
Some checks are pending
CI / test (push) Waiting to run
This commit is contained in:
parent
b8d894f243
commit
35211c69a4
@ -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<ReturnType<typeof analyze>>): 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<string, unknown>;
|
||||
const sorted = Object.keys(record)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.reduce<Record<string, unknown>>((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 (
|
||||
<div className="app-shell">
|
||||
<TopBar
|
||||
@ -185,7 +274,23 @@ export function App() {
|
||||
}
|
||||
/>
|
||||
<CanvasArea store={store} />
|
||||
<RightInspector model={model} selection={selection} compileResult={compileResult} />
|
||||
<RightInspector
|
||||
model={model}
|
||||
selection={selection}
|
||||
compileResult={compileResult}
|
||||
jsonText={jsonEditorText}
|
||||
jsonError={jsonError}
|
||||
onJsonTextChange={setJsonEditorText}
|
||||
onApplyJson={applyJsonFromEditor}
|
||||
onValidateJson={() => {
|
||||
void validateJsonEditor();
|
||||
}}
|
||||
onFormatJson={formatJsonEditor}
|
||||
onSortJson={sortJsonEditorKeys}
|
||||
onCopyRepro={() => {
|
||||
void copyReproJson();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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<string, unknown> | 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
|
||||
|
||||
<h2 className="panel__title">Layout Metrics</h2>
|
||||
<pre>{toSnippet(layoutMetrics, "No layout metrics available yet.")}</pre>
|
||||
|
||||
<h2 className="panel__title">JSON Editor</h2>
|
||||
<div className="json-tools">
|
||||
<button type="button" onClick={onValidateJson}>
|
||||
Validate
|
||||
</button>
|
||||
<button type="button" onClick={onFormatJson}>
|
||||
Format
|
||||
</button>
|
||||
<button type="button" onClick={onSortJson}>
|
||||
Sort Keys
|
||||
</button>
|
||||
<button type="button" onClick={onCopyRepro}>
|
||||
Copy Repro
|
||||
</button>
|
||||
<button type="button" onClick={onApplyJson}>
|
||||
Apply JSON
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
className="json-editor"
|
||||
value={jsonText}
|
||||
onChange={(event) => onJsonTextChange(event.target.value)}
|
||||
spellCheck={false}
|
||||
aria-label="Schematic JSON editor"
|
||||
/>
|
||||
<p className={`panel__body ${jsonError ? "json-status--error" : "json-status--ok"}`}>
|
||||
{jsonError ? `JSON error: ${jsonError}` : "JSON ready"}
|
||||
</p>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@ -190,6 +190,49 @@ body {
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.json-tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.json-tools button {
|
||||
padding: 0.28rem 0.48rem;
|
||||
border: 1px solid #b9c9d6;
|
||||
border-radius: 6px;
|
||||
background: #f8fbff;
|
||||
color: #173247;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.json-tools button:hover {
|
||||
background: #eaf2f8;
|
||||
}
|
||||
|
||||
.json-editor {
|
||||
width: 100%;
|
||||
min-height: 220px;
|
||||
border: 1px solid #c1d0dd;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
background: #fbfdff;
|
||||
color: #173247;
|
||||
font-family: "IBM Plex Mono", "Fira Code", monospace;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.json-status--ok {
|
||||
color: #12633c;
|
||||
}
|
||||
|
||||
.json-status--error {
|
||||
color: #a31515;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user