Extracts the Ace integration #2094 carried inline into a shared `elements/JsonEditor`, and renders it on every surface where the web client types or displays JSON. The element is deliberately text in, text out. The two editing contracts above it disagree about what an unparseable draft means — JsonObjectInput keeps the last valid object and says nothing to its parent, SchemaJsonField reports `undefined` and blocks submission (#2020) — so parsing, and the draft/value split that goes with it, stays with each caller. Surfaces: SchemaJsonField (inherited by all four SchemaForm consumers), a new "Edit as JSON" switch for the whole arguments object, the server.json File Contents box, the raw JSON-RPC request box, ContentViewer's JSON branch (read-only, inherited by its eleven consumers), and a new "Edit and replay" modal on a Protocol entry. Ace read-only replaces the CodeHighlight JSON path and the Prism `json` grammar loader is dropped: folding, not colorization, is what it adds, and two highlighters for one language drift. `wrap={false}` callers and untyped text that only looks like JSON keep the plain renderer. Closes #2151 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com>
133 lines
4 KiB
TypeScript
133 lines
4 KiB
TypeScript
import { useState } from "react";
|
|
import { Button, Group, Modal, Stack, Text } from "@mantine/core";
|
|
import { JsonEditor } from "../../elements/JsonEditor/JsonEditor";
|
|
import { isJsonObject } from "../../../utils/jsonObjectDraft";
|
|
|
|
export interface EditReplayModalProps {
|
|
opened: boolean;
|
|
/** The method being replayed, shown so the editor is unambiguous. */
|
|
method: string;
|
|
/** The originating entry's params, seeded into the editor. */
|
|
params?: Record<string, unknown>;
|
|
/**
|
|
* Re-issue the request with these params. `null` means "send no params",
|
|
* which is a request an empty editor can legitimately express — see
|
|
* {@link ReplayParamsOverride} for why it is not `undefined`.
|
|
*/
|
|
onSend: (params: Record<string, unknown> | null) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const MethodText = Text.withProps({
|
|
size: "sm",
|
|
c: "dimmed",
|
|
ff: "monospace",
|
|
});
|
|
|
|
const Footer = Group.withProps({ justify: "flex-end", gap: "sm" });
|
|
|
|
/**
|
|
* What the editor's text currently means: the params to send, or why they
|
|
* cannot be sent.
|
|
*
|
|
* Empty text is "no params" rather than an error — a `tools/list` replay
|
|
* genuinely carries none, and an empty editor wearing a red error would read as
|
|
* broken. A non-object is reported separately from text that is not JSON at
|
|
* all: JSON-RPC `params` must be a structured value, so `42` is a shape that
|
|
* cannot be sent however finished it is.
|
|
*/
|
|
function parseParamsDraft(
|
|
text: string,
|
|
):
|
|
| { ok: true; value: Record<string, unknown> | null }
|
|
| { ok: false; error: string } {
|
|
if (text.trim() === "") return { ok: true, value: null };
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch {
|
|
return { ok: false, error: "Not valid JSON — this cannot be sent" };
|
|
}
|
|
if (!isJsonObject(parsed)) {
|
|
return { ok: false, error: "Params must be a JSON object (`{ … }`)" };
|
|
}
|
|
return { ok: true, value: parsed };
|
|
}
|
|
|
|
/**
|
|
* The modal's body, split out so its draft is owned by a component Mantine
|
|
* unmounts on close — which is what makes reopening start from the entry's
|
|
* params again rather than from whatever was last typed and abandoned.
|
|
*/
|
|
function EditReplayForm({
|
|
method,
|
|
params,
|
|
onSend,
|
|
onClose,
|
|
}: Omit<EditReplayModalProps, "opened">) {
|
|
const [draft, setDraft] = useState(() =>
|
|
params === undefined ? "" : JSON.stringify(params, null, 2),
|
|
);
|
|
const parsed = parseParamsDraft(draft);
|
|
|
|
return (
|
|
<Stack gap="sm">
|
|
<MethodText>{method}</MethodText>
|
|
<JsonEditor
|
|
ariaLabel="Request params"
|
|
label="Params"
|
|
value={draft}
|
|
onChange={setDraft}
|
|
error={parsed.ok ? undefined : parsed.error}
|
|
minLines={8}
|
|
maxLines={24}
|
|
/>
|
|
<Footer>
|
|
<Button variant="default" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
// Unlike the metadata editor, this modal *has* a commit gesture to
|
|
// gate — so invalid text disables Send rather than quietly sending
|
|
// the last value that parsed. Same reasoning as `SchemaJsonField`.
|
|
disabled={!parsed.ok}
|
|
onClick={() => {
|
|
/* v8 ignore next -- Send is disabled while the draft is invalid */
|
|
if (!parsed.ok) return;
|
|
onSend(parsed.value);
|
|
onClose();
|
|
}}
|
|
>
|
|
Send
|
|
</Button>
|
|
</Footer>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Edit a Protocol entry's request params, then replay it (#2151).
|
|
*
|
|
* Replay itself is one click and unchanged; this is a second way to *supply*
|
|
* the params, not a second replay implementation — Send dispatches through the
|
|
* same `replayProtocolRequest` the plain Replay button does, so the same
|
|
* method gate and the same "Can't replay" failure toast apply.
|
|
*/
|
|
export function EditReplayModal({
|
|
opened,
|
|
method,
|
|
params,
|
|
onSend,
|
|
onClose,
|
|
}: EditReplayModalProps) {
|
|
return (
|
|
<Modal opened={opened} onClose={onClose} title="Edit and replay" size="lg">
|
|
<EditReplayForm
|
|
method={method}
|
|
params={params}
|
|
onSend={onSend}
|
|
onClose={onClose}
|
|
/>
|
|
</Modal>
|
|
);
|
|
}
|