* feat(web): add Mantine clear buttons to text inputs app-wide (#1333) Add a clear (×) affordance to every user-editable text input across the web client so a populated field can be reset in one click. - Autocomplete: enable `clearable` by default via the theme (`theme/Autocomplete.ts`) — covers the prompt-argument and resource-template variable autocomplete branches. - TextInput / Textarea: Mantine v8 does not expose a public `clearable` prop on these primitives (only the combobox-based components do), so add the clear button manually via `rightSection` + `CloseButton` (aria-label="Clear") shown only when the field has a value, wired to reset the field's controlled value to "". Applied to sidebar search inputs (prompts, tools, apps, tasks, history, resources, network, logs), prompt-argument and resource-template variable inputs, the sampling response/model fields, server config & settings forms, roots table, schema form, experimental features panel, and the server.json import panel. Read-only/display Textareas (OutputValidationModal, UrlElicitationErrorModal) are intentionally left untouched. Tests cover the clear behavior on representative inputs from each category (sidebar search, prompt argument, resource-template variable, sampling response, plus the config/settings forms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): disable ServerConfigModal clear buttons while submitting Address review #2 on #1448: the clear CloseButton sat in `rightSection` with `rightSectionPointerEvents="auto"`, so it stayed clickable even while the field was `disabled={submitting}`, letting a user wipe a field mid-submit. Forward `disabled={submitting}` to each clear button so it locks together with its input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): remove redundant Auto-respond from SamplingRequestPanel The sampling modal pre-loads its draft with the default stub result, and "Send Response" sends that draft — so on an untouched form "Auto-respond" did exactly the same thing as "Send Response", just with a fresh copy of the same defaults. Drop the redundant button and its `onAutoRespond` prop; "Send Response" now covers both the unedited (stub) and edited cases. The InlineSamplingRequest variant keeps its Auto-respond button — there it is the quick path versus "Edit & Send" (which opens this editable modal), not a duplicate of an adjacent send action. Updated PendingClientRequestModal wiring, stories, and tests accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
62d442b073
commit
0890c7509a
2 changed files with 805 additions and 28 deletions
|
|
@ -1,10 +1,18 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { waitFor, within } from "@testing-library/react";
|
||||
import type { MCPServerConfig } from "@inspector/core/mcp/types.js";
|
||||
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
||||
import { ServerConfigModal } from "./ServerConfigModal";
|
||||
|
||||
/** Find the "Clear" button living in the rightSection of `input`'s field. */
|
||||
function clearButtonFor(input: HTMLElement): HTMLElement {
|
||||
const root =
|
||||
input.closest('[class*="mantine-TextInput-root"]') ??
|
||||
input.closest('[class*="Input-wrapper"]');
|
||||
return within(root as HTMLElement).getByRole("button", { name: "Clear" });
|
||||
}
|
||||
|
||||
describe("ServerConfigModal", () => {
|
||||
function base(overrides: Partial<{ existingIds: string[] }> = {}) {
|
||||
return {
|
||||
|
|
@ -274,6 +282,114 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("clears the Server ID field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
mode="edit"
|
||||
initialId="alpha"
|
||||
initialConfig={{ type: "stdio", command: "node" }}
|
||||
/>,
|
||||
);
|
||||
const idInput = screen.getByLabelText(/Server ID/i);
|
||||
expect(idInput).toHaveValue("alpha");
|
||||
await user.click(clearButtonFor(idInput));
|
||||
expect(idInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("clears the Command field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
mode="edit"
|
||||
initialId="alpha"
|
||||
initialConfig={{ type: "stdio", command: "node" }}
|
||||
/>,
|
||||
);
|
||||
const cmdInput = screen.getByLabelText(/Command/i);
|
||||
expect(cmdInput).toHaveValue("node");
|
||||
await user.click(clearButtonFor(cmdInput));
|
||||
expect(cmdInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("clears the URL field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
mode="edit"
|
||||
initialId="remote"
|
||||
initialConfig={{ type: "sse", url: "https://x.test/sse" }}
|
||||
/>,
|
||||
);
|
||||
const urlInput = screen.getByLabelText(/^URL/);
|
||||
expect(urlInput).toHaveValue("https://x.test/sse");
|
||||
await user.click(clearButtonFor(urlInput));
|
||||
expect(urlInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("clears the Arguments field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
mode="edit"
|
||||
initialId="alpha"
|
||||
initialConfig={{
|
||||
type: "stdio",
|
||||
command: "node",
|
||||
args: ["server.js", "--port=3000"],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const argsInput = screen.getByLabelText(/Arguments/i);
|
||||
expect(argsInput).toHaveValue("server.js\n--port=3000");
|
||||
await user.click(clearButtonFor(argsInput));
|
||||
expect(argsInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("clears the Environment field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
mode="edit"
|
||||
initialId="alpha"
|
||||
initialConfig={{
|
||||
type: "stdio",
|
||||
command: "node",
|
||||
env: { DEBUG: "1" },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const envInput = screen.getByLabelText(/Environment/i);
|
||||
expect(envInput).toHaveValue("DEBUG=1");
|
||||
await user.click(clearButtonFor(envInput));
|
||||
expect(envInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("clears the Working directory field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
mode="edit"
|
||||
initialId="alpha"
|
||||
initialConfig={{
|
||||
type: "stdio",
|
||||
command: "node",
|
||||
cwd: "/tmp/here",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const cwdInput = screen.getByLabelText(/Working directory/i);
|
||||
expect(cwdInput).toHaveValue("/tmp/here");
|
||||
await user.click(clearButtonFor(cwdInput));
|
||||
expect(cwdInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("calls onClose when Cancel is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const props = base();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
CloseButton,
|
||||
Group,
|
||||
Modal,
|
||||
Select,
|
||||
|
|
@ -275,6 +276,16 @@ export function ServerConfigModal({
|
|||
data-autofocus
|
||||
required
|
||||
disabled={submitting}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
form.id ? (
|
||||
<CloseButton
|
||||
aria-label="Clear"
|
||||
disabled={submitting}
|
||||
onClick={() => setForm((f) => ({ ...f, id: "" }))}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
<Select
|
||||
|
|
@ -307,6 +318,16 @@ export function ServerConfigModal({
|
|||
}}
|
||||
required
|
||||
disabled={submitting}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
form.command ? (
|
||||
<CloseButton
|
||||
aria-label="Clear"
|
||||
disabled={submitting}
|
||||
onClick={() => setForm((f) => ({ ...f, command: "" }))}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Textarea
|
||||
label="Arguments"
|
||||
|
|
@ -320,6 +341,16 @@ export function ServerConfigModal({
|
|||
autosize
|
||||
minRows={3}
|
||||
disabled={submitting}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
form.argsText ? (
|
||||
<CloseButton
|
||||
aria-label="Clear"
|
||||
disabled={submitting}
|
||||
onClick={() => setForm((f) => ({ ...f, argsText: "" }))}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Textarea
|
||||
label="Environment"
|
||||
|
|
@ -333,6 +364,16 @@ export function ServerConfigModal({
|
|||
autosize
|
||||
minRows={2}
|
||||
disabled={submitting}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
form.envText ? (
|
||||
<CloseButton
|
||||
aria-label="Clear"
|
||||
disabled={submitting}
|
||||
onClick={() => setForm((f) => ({ ...f, envText: "" }))}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Working directory"
|
||||
|
|
@ -343,6 +384,16 @@ export function ServerConfigModal({
|
|||
setForm((f) => ({ ...f, cwd: next }));
|
||||
}}
|
||||
disabled={submitting}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
form.cwd ? (
|
||||
<CloseButton
|
||||
aria-label="Clear"
|
||||
disabled={submitting}
|
||||
onClick={() => setForm((f) => ({ ...f, cwd: "" }))}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -356,6 +407,16 @@ export function ServerConfigModal({
|
|||
}}
|
||||
required
|
||||
disabled={submitting}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
form.url ? (
|
||||
<CloseButton
|
||||
aria-label="Clear"
|
||||
disabled={submitting}
|
||||
onClick={() => setForm((f) => ({ ...f, url: "" }))}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FieldGrid>
|
||||
|
|
|
|||
Loading…
Reference in a new issue