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 { analyze, autoLayout, compile, tidyLayout } from "./api/client";
|
||||||
import { CanvasArea } from "./components/CanvasArea";
|
import { CanvasArea } from "./components/CanvasArea";
|
||||||
import { LeftPanel } from "./components/LeftPanel";
|
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() {
|
export function App() {
|
||||||
const store = useMemo(() => createSchemetaStore({ model: SAMPLE_PAYLOAD }), []);
|
const store = useMemo(() => createSchemetaStore({ model: SAMPLE_PAYLOAD }), []);
|
||||||
const actionsApi = useSchemetaActions(store);
|
const actionsApi = useSchemetaActions(store);
|
||||||
@ -48,6 +65,13 @@ export function App() {
|
|||||||
const compileResult = useSchemetaSelector(store, (state) => state.compileResult);
|
const compileResult = useSchemetaSelector(store, (state) => state.compileResult);
|
||||||
const lifecycle = useSchemetaSelector(store, (state) => state.lifecycle);
|
const lifecycle = useSchemetaSelector(store, (state) => state.lifecycle);
|
||||||
const jsonError = useSchemetaSelector(store, (state) => state.uiFlags.jsonError);
|
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 lastApiVersion = useSchemetaSelector(store, (state) => {
|
||||||
const apiVersion = state.compileResult && typeof state.compileResult.api_version === "string" ? state.compileResult.api_version : null;
|
const apiVersion = state.compileResult && typeof state.compileResult.api_version === "string" ? state.compileResult.api_version : null;
|
||||||
return apiVersion ?? "-";
|
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 (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<TopBar
|
<TopBar
|
||||||
@ -185,7 +274,23 @@ export function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CanvasArea store={store} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,14 @@ type RightInspectorProps = {
|
|||||||
model: SchemetaModel | null;
|
model: SchemetaModel | null;
|
||||||
selection: SelectionSlice;
|
selection: SelectionSlice;
|
||||||
compileResult: unknown | null;
|
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 {
|
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 selectedRef = selection.selectedRefs[0] ?? null;
|
||||||
const selectedInstance = selectedRef ? (model?.instances ?? []).find((instance) => instance.ref === selectedRef) : 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>
|
<h2 className="panel__title">Layout Metrics</h2>
|
||||||
<pre>{toSnippet(layoutMetrics, "No layout metrics available yet.")}</pre>
|
<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>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -190,6 +190,49 @@ body {
|
|||||||
font-size: 0.76rem;
|
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 {
|
.canvas {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user