feat(web): use the Ace editor for the request-metadata JSON
A textarea is a poor place to hand-write a nested `_meta` payload. Ace closes a
brace when you open one, folds nested objects, and — through its JSON worker —
marks the offending line in the gutter instead of reporting one message for the
whole document.
Three things the swap turned up:
- Ace fires *two* change events for a replace (a remove, then an insert), so
select-all-and-retype briefly leaves an empty document. Acting on the first
emitted `{}` and wiped the user's saved metadata the instant they selected
it. `handleChange` now coalesces the pair in a microtask, and the component
tracks what it last emitted so the parent echoing that value back is not
mistaken for an external reset.
- Ace's own `github` themes fail WCAG AA on the gutter — `#AAA` on `#e8e8e8` is
1.89:1, and the dark theme is 4.13:1. Folding needs the gutter, so the colors
are overridden with palette tokens (5.82:1 and 4.88:1) rather than hiding it.
- `textInputAriaLabel` alone never reaches the DOM: Ace composes the label as
"<label>, Cursor at row N" and only recomputes it when the cursor moves, so
it is forced once on load. Until then the control's only accessible name is a
position readout.
Testing is split because Ace's input path does not work under happy-dom at all
— `userEvent.type` reaches the textarea and produces no edit, so a test written
that way asserts nothing. Unit tests drive the editor instance through a shared
`src/test/aceEditor.ts` helper; the real keyboard behaviour (`{` then Enter
giving `{\n \n}`) is asserted by Storybook play functions in Chromium.
`react-ace`/`ace-builds` are declared in `clients/web` rather than the root, per
the rule for a dependency that renders React components.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
b0ab6842aa
commit
ed799a0c0b
2 changed files with 432 additions and 94 deletions
|
|
@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
|
|||
import { within } from "@testing-library/react";
|
||||
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
|
||||
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
||||
import { getAceText, setAceText } from "../../../test/aceEditor";
|
||||
import {
|
||||
ServerSettingsForm,
|
||||
type ServerSettingsSection,
|
||||
|
|
@ -249,8 +250,8 @@ describe("ServerSettingsForm", () => {
|
|||
screen.getByText("No custom headers configured"),
|
||||
).toBeInTheDocument();
|
||||
// Metadata has no rows to be absent — the editor is always present, and
|
||||
// "none configured" is spelled `{}` in the box itself.
|
||||
expect(screen.getByLabelText("Request metadata JSON")).toHaveValue("{}");
|
||||
// "none configured" is spelled `{}` in it.
|
||||
expect(getAceText()).toBe("{}");
|
||||
});
|
||||
|
||||
it("invokes onAddHeader when + Add Header is clicked", async () => {
|
||||
|
|
@ -331,17 +332,15 @@ describe("ServerSettingsForm", () => {
|
|||
expandedSections={["metadata"]}
|
||||
/>,
|
||||
);
|
||||
const editor = screen.getByLabelText("Request metadata JSON");
|
||||
// The point of #1910: a nested object survives into the editor instead of
|
||||
// being flattened into a `{ key, value }` string row.
|
||||
expect(JSON.parse((editor as HTMLTextAreaElement).value)).toEqual({
|
||||
expect(JSON.parse(getAceText())).toEqual({
|
||||
userId: "u-1",
|
||||
tenant: { id: 7, tags: ["a"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the whole edited object through onMetadataChange", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onMetadataChange = vi.fn();
|
||||
renderWithMantine(
|
||||
<ServerSettingsForm
|
||||
|
|
@ -351,9 +350,7 @@ describe("ServerSettingsForm", () => {
|
|||
expandedSections={["metadata"]}
|
||||
/>,
|
||||
);
|
||||
const editor = screen.getByLabelText("Request metadata JSON");
|
||||
await user.clear(editor);
|
||||
await user.type(editor, '{{"n":[[1,2]}');
|
||||
await setAceText('{"n":[1,2]}');
|
||||
expect(onMetadataChange).toHaveBeenLastCalledWith({ n: [1, 2] });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -735,8 +735,7 @@ export function ServerSettingsForm({
|
|||
strings. Leave it as <Code>{"{}"}</Code> to send none.
|
||||
</HintText>
|
||||
<JsonObjectInput
|
||||
aria-label="Request metadata JSON"
|
||||
placeholder={'{\n "example": { "nested": true }\n}'}
|
||||
ariaLabel="Request metadata JSON"
|
||||
value={settings.metadata}
|
||||
onChange={onMetadataChange}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue