From c1362b871da1c17f211744903065b27f35f9d3e0 Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Mon, 24 Aug 2026 11:46:04 +0100 Subject: [PATCH 1/2] fix: show copy button confirmation on click only Signed-off-by: Marek Dano --- package-lock.json | 68 +++++++++++++++ package.json | 1 + .../VirtualServerDetailsPanel.test.tsx | 4 +- .../gateways/VirtualServerDetailsPanel.tsx | 12 ++- src/components/prompts/PromptCodeTab.test.tsx | 8 +- .../prompts/PromptSnippetTabs.test.tsx | 2 +- .../resources/ResourcePreviewResult.tsx | 8 +- .../resources/ResourcesTable.test.tsx | 6 +- src/components/resources/ResourcesTable.tsx | 7 +- .../servers/MCPServerDetailsPanel.test.tsx | 5 +- .../servers/MCPServerDetailsPanel.tsx | 16 ++-- src/components/servers/ServersTable.test.tsx | 10 ++- .../tokens/TokenCreatedDialog.test.tsx | 6 ++ src/components/tokens/TokenCreatedDialog.tsx | 7 -- src/components/tools/ToolForm.test.tsx | 8 +- .../tools/ToolSchemaDialog.test.tsx | 2 +- src/components/tools/ToolSchemaDialog.tsx | 15 +++- src/components/tools/ToolsTable.test.tsx | 4 +- src/components/tools/ToolsTable.tsx | 5 +- src/components/ui/code-block.test.tsx | 4 +- src/components/ui/code-block.tsx | 9 +- src/components/ui/copy-button.test.tsx | 32 ++++++- src/components/ui/copy-button.tsx | 86 +++++++++++-------- src/i18n/locales/en-US/common.json | 1 + src/i18n/locales/en-US/gateways.json | 1 + src/i18n/locales/en-US/mcpServer.json | 2 + src/i18n/locales/en-US/resources.json | 5 +- src/i18n/locales/en-US/tools.json | 4 +- src/i18n/locales/es-ES/common.json | 1 + src/i18n/locales/es-ES/gateways.json | 1 + src/i18n/locales/es-ES/mcpServer.json | 2 + src/i18n/locales/es-ES/resources.json | 5 +- src/i18n/locales/es-ES/tools.json | 4 +- src/i18n/locales/pt-BR/common.json | 1 + src/i18n/locales/pt-BR/gateways.json | 1 + src/i18n/locales/pt-BR/mcpServer.json | 2 + src/i18n/locales/pt-BR/resources.json | 5 +- src/i18n/locales/pt-BR/tools.json | 4 +- 38 files changed, 266 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4f0f17..2fa2782 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "mcp-context-forge-ui", "version": "0.1.0", "dependencies": { + "@floating-ui/react-dom": "^2.1.8", "@fontsource-variable/inter": "^5.2.8", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.2", @@ -56,6 +57,9 @@ "typescript-eslint": "^8.32.0", "vite": "^6.4.3", "vitest": "^4.1.9" + }, + "engines": { + "node": ">=22.22.1" } }, "node_modules/@adobe/css-tools": { @@ -4866,6 +4870,70 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", diff --git a/package.json b/package.json index 73c8b84..fda59e9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "prepare": "husky || true" }, "dependencies": { + "@floating-ui/react-dom": "^2.1.8", "@fontsource-variable/inter": "^5.2.8", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.2", diff --git a/src/components/gateways/VirtualServerDetailsPanel.test.tsx b/src/components/gateways/VirtualServerDetailsPanel.test.tsx index 63db860..b88be95 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.test.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.test.tsx @@ -143,7 +143,9 @@ describe("VirtualServerDetailsPanel components list", () => { await user.click(screen.getByRole("button", { name: "Copy Titled Tool" })); expect(copyToClipboard).toHaveBeenCalledWith("titled-tool-id"); - await user.click(screen.getByRole("button", { name: "Copy Plain Tool" })); + // The untitled row has no separate name to reference, so its copy label + // uses the component-type noun rather than the raw identifier value. + await user.click(screen.getByRole("button", { name: "Copy tool" })); expect(copyToClipboard).toHaveBeenCalledWith("Plain Tool"); }); diff --git a/src/components/gateways/VirtualServerDetailsPanel.tsx b/src/components/gateways/VirtualServerDetailsPanel.tsx index 39dce7a..5ed1ce6 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.tsx @@ -603,7 +603,7 @@ export function VirtualServerDetailsPanel({ value={identifier} label={intl.formatMessage( { id: "common.copyValue" }, - { label: identifier }, + { label: getComponentLabel(component.type) }, )} className="size-5 text-muted-foreground" /> @@ -674,10 +674,16 @@ export function VirtualServerDetailsPanel({ {server.version ?? intl.formatMessage({ id: "gateways.details.notAvailable" })} - + - + {(() => { const tagLabels = tags.map((tag) => tag.label); diff --git a/src/components/prompts/PromptCodeTab.test.tsx b/src/components/prompts/PromptCodeTab.test.tsx index 3d32085..8991627 100644 --- a/src/components/prompts/PromptCodeTab.test.tsx +++ b/src/components/prompts/PromptCodeTab.test.tsx @@ -112,13 +112,15 @@ describe("PromptCodeTab", () => { await waitFor(() => expect(screen.getByRole("button", { name: /re-run/i })).toBeInTheDocument(), ); - // Status row is announced live once the run completes. - expect(screen.getByRole("status")).toBeInTheDocument(); + // Status row is announced live once the run completes. (Copy buttons on + // the page also mount a plain, unlabelled `role="status"` region, so + // disambiguate on `aria-live`, which only the preview status row sets.) + expect(document.querySelector('[role="status"][aria-live="polite"]')).toBeInTheDocument(); await user.click(screen.getByRole("tab", { name: "Python" })); expect(screen.getByRole("button", { name: /^preview$/i })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /re-run/i })).not.toBeInTheDocument(); - expect(screen.queryByRole("status")).not.toBeInTheDocument(); + expect(document.querySelector('[role="status"][aria-live="polite"]')).not.toBeInTheDocument(); }); }); diff --git a/src/components/prompts/PromptSnippetTabs.test.tsx b/src/components/prompts/PromptSnippetTabs.test.tsx index ef7a144..8f8f226 100644 --- a/src/components/prompts/PromptSnippetTabs.test.tsx +++ b/src/components/prompts/PromptSnippetTabs.test.tsx @@ -81,7 +81,7 @@ describe("PromptSnippetTabs", () => { expect(writeText).toHaveBeenCalledTimes(1); expect(writeText.mock.calls[0][0]).toContain("curl -X POST"); expect(writeText.mock.calls[0][0]).toContain('"user":"Alice"'); - expect(screen.getByRole("tooltip")).toHaveTextContent("Copied!"); + expect(screen.getByRole("status")).toHaveTextContent("Copied!"); }); it("rebuilds the snippet when args change", () => { diff --git a/src/components/resources/ResourcePreviewResult.tsx b/src/components/resources/ResourcePreviewResult.tsx index a09a3d0..168bc4a 100644 --- a/src/components/resources/ResourcePreviewResult.tsx +++ b/src/components/resources/ResourcePreviewResult.tsx @@ -163,7 +163,13 @@ function ResourceContentPreview({ ); } diff --git a/src/components/resources/ResourcesTable.test.tsx b/src/components/resources/ResourcesTable.test.tsx index b980857..1f0f35d 100644 --- a/src/components/resources/ResourcesTable.test.tsx +++ b/src/components/resources/ResourcesTable.test.tsx @@ -169,7 +169,7 @@ describe("ResourcesTable", () => { const resources = [createMockResource(1, { uri: "resource://my-uri" })]; render(); - const copyButton = screen.getByLabelText("Copy resource://my-uri"); + const copyButton = screen.getByLabelText("Copy URI for Resource 1 Title"); await user.click(copyButton); expect(clipboardUtils.copyToClipboard).toHaveBeenCalledWith("resource://my-uri"); @@ -180,7 +180,7 @@ describe("ResourcesTable", () => { const resources = [createMockResource(1)]; render(); - const copyButton = screen.getByLabelText(/^Copy resource:\/\//); + const copyButton = screen.getByLabelText(/^Copy URI for/); await user.click(copyButton); expect(mockOnSelectResource).not.toHaveBeenCalled(); @@ -191,7 +191,7 @@ describe("ResourcesTable", () => { const resources = [createMockResource(1, { id: "resource-abc-123" })]; render(); - const copyButton = screen.getByLabelText("Copy resource ID"); + const copyButton = screen.getByLabelText("Copy resource ID for Resource 1 Title"); await user.click(copyButton); expect(clipboardUtils.copyToClipboard).toHaveBeenCalledWith("resource-abc-123"); diff --git a/src/components/resources/ResourcesTable.tsx b/src/components/resources/ResourcesTable.tsx index bda0e66..68e2c3a 100644 --- a/src/components/resources/ResourcesTable.tsx +++ b/src/components/resources/ResourcesTable.tsx @@ -82,7 +82,7 @@ export function ResourcesTable({ value={resource.uriTemplate || resource.uri} label={intl.formatMessage( { id: "resources.table.copyUri" }, - { uri: resource.uriTemplate || resource.uri }, + { name: resource.title || resource.name }, )} iconClassName="size-3" className="ml-4 size-4 shrink-0 text-muted-foreground hover:text-foreground" @@ -97,7 +97,10 @@ export function ResourcesTable({ diff --git a/src/components/servers/MCPServerDetailsPanel.test.tsx b/src/components/servers/MCPServerDetailsPanel.test.tsx index 2905baf..68b6383 100644 --- a/src/components/servers/MCPServerDetailsPanel.test.tsx +++ b/src/components/servers/MCPServerDetailsPanel.test.tsx @@ -817,11 +817,12 @@ describe("MCPServerDetailsPanel", () => { />, ); - // A titled tool copies via its title label; an untitled one via its identifier. + // A titled tool copies via its title label; an untitled one via the + // component-type noun (its identifier is a raw value, not a name). await user.click(await screen.findByRole("button", { name: "Copy Tool One" })); expect(copyToClipboard).toHaveBeenCalledWith("original_tool_1"); - await user.click(screen.getByRole("button", { name: "Copy original_tool_2" })); + await user.click(screen.getByRole("button", { name: "Copy tool" })); expect(copyToClipboard).toHaveBeenCalledWith("original_tool_2"); }); diff --git a/src/components/servers/MCPServerDetailsPanel.tsx b/src/components/servers/MCPServerDetailsPanel.tsx index b74a5d2..6769d9e 100644 --- a/src/components/servers/MCPServerDetailsPanel.tsx +++ b/src/components/servers/MCPServerDetailsPanel.tsx @@ -506,7 +506,7 @@ export function MCPServerDetailsPanel({ value={identifier} label={intl.formatMessage( { id: "common.copyValue" }, - { label: identifier }, + { label: component.type.slice(0, -1) }, )} className="size-5 text-muted-foreground" /> @@ -568,13 +568,19 @@ export function MCPServerDetailsPanel({ {getTransportLabel(server.transport)} - - + + - + - + {server.team && ( diff --git a/src/components/servers/ServersTable.test.tsx b/src/components/servers/ServersTable.test.tsx index 192edc7..44d8869 100644 --- a/src/components/servers/ServersTable.test.tsx +++ b/src/components/servers/ServersTable.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ServersTable } from "./ServersTable"; @@ -38,6 +38,10 @@ describe("ServersTable", () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.useRealTimers(); + }); + // ── Loading state ──────────────────────────────────────────────────────────── it("renders a loading indicator when isLoading is true", () => { @@ -200,9 +204,7 @@ describe("ServersTable", () => { act(() => { vi.advanceTimersByTime(1500); }); - expect(screen.queryByRole("status")).not.toBeInTheDocument(); - - vi.useRealTimers(); + expect(screen.getByRole("status")).toHaveTextContent(""); }); it("shows a failed indicator instead of throwing when copying to the clipboard fails", async () => { diff --git a/src/components/tokens/TokenCreatedDialog.test.tsx b/src/components/tokens/TokenCreatedDialog.test.tsx index a29f405..52c1924 100644 --- a/src/components/tokens/TokenCreatedDialog.test.tsx +++ b/src/components/tokens/TokenCreatedDialog.test.tsx @@ -79,6 +79,12 @@ describe("TokenCreatedDialog", () => { expect(onClose).toHaveBeenCalledTimes(1); }); + it("auto-focuses the copy button on open (no redirect needed without a hover tooltip)", async () => { + renderWithProviders(); + + await waitFor(() => expect(screen.getByRole("button", { name: /copy/i })).toHaveFocus()); + }); + it("calls copyToClipboard with the token when the copy button is clicked", () => { renderWithProviders(); fireEvent.click(screen.getByRole("button", { name: /copy/i })); diff --git a/src/components/tokens/TokenCreatedDialog.tsx b/src/components/tokens/TokenCreatedDialog.tsx index a59b42c..49becb8 100644 --- a/src/components/tokens/TokenCreatedDialog.tsx +++ b/src/components/tokens/TokenCreatedDialog.tsx @@ -44,13 +44,6 @@ export function TokenCreatedDialog({ token, onClose, returnFocusRef }: TokenCrea > { - // Radix would otherwise auto-focus the first focusable descendant, - // the copy button — whose tooltip opens on focus and would eat the - // first Escape press instead of the dialog. Focus Close instead. - event.preventDefault(); - closeButtonRef.current?.focus(); - }} onCloseAutoFocus={(event) => { const target = returnFocusRef?.current; if (!target) return; // No target: leave Radix's default restore alone. diff --git a/src/components/tools/ToolForm.test.tsx b/src/components/tools/ToolForm.test.tsx index 64a06fe..af38472 100644 --- a/src/components/tools/ToolForm.test.tsx +++ b/src/components/tools/ToolForm.test.tsx @@ -666,9 +666,13 @@ describe("ToolForm", () => { expect(writeTextMock).toHaveBeenCalledWith('{\n "type": "string"\n}'); - // Wait for the copied confirmation to appear + // Wait for the copied confirmation to appear. Both the input- and + // output-schema copy buttons mount their own status region, so find + // the one that actually announces "Copied!" rather than assuming + // there's only one on the page. await waitFor(() => { - expect(screen.getByRole("status")).toHaveTextContent("Copied!"); + const statuses = screen.getAllByRole("status"); + expect(statuses.some((el) => el.textContent === "Copied!")).toBe(true); }); // Restore clipboard diff --git a/src/components/tools/ToolSchemaDialog.test.tsx b/src/components/tools/ToolSchemaDialog.test.tsx index b46c188..a185040 100644 --- a/src/components/tools/ToolSchemaDialog.test.tsx +++ b/src/components/tools/ToolSchemaDialog.test.tsx @@ -272,7 +272,7 @@ describe("ToolSchemaDialog", () => { }); }); - it("closes on a single Escape press instead of dismissing a copy button tooltip first", async () => { + it("closes on a single Escape press", async () => { const user = userEvent.setup(); const tool = createMockTool(); render(); diff --git a/src/components/tools/ToolSchemaDialog.tsx b/src/components/tools/ToolSchemaDialog.tsx index e788b12..729e947 100644 --- a/src/components/tools/ToolSchemaDialog.tsx +++ b/src/components/tools/ToolSchemaDialog.tsx @@ -50,6 +50,7 @@ function SchemaSection({ } export function ToolSchemaDialog({ tool, open, onOpenChange }: ToolSchemaDialogProps) { + const intl = useIntl(); const closeButtonRef = useRef(null); return ( @@ -58,8 +59,8 @@ export function ToolSchemaDialog({ tool, open, onOpenChange }: ToolSchemaDialogP className="max-w-2xl" onOpenAutoFocus={(event) => { // Radix would otherwise auto-focus the first focusable descendant, - // a schema copy button — whose tooltip opens on focus and would eat - // the first Escape press instead of the dialog. Focus Close instead. + // a schema copy button buried below the input/output JSON. Close is + // the more useful place to land in a dialog that's mostly read-only. event.preventDefault(); closeButtonRef.current?.focus(); }} @@ -80,8 +81,14 @@ export function ToolSchemaDialog({ tool, open, onOpenChange }: ToolSchemaDialogP
- - + +
diff --git a/src/components/tools/ToolsTable.test.tsx b/src/components/tools/ToolsTable.test.tsx index bf2f867..a481218 100644 --- a/src/components/tools/ToolsTable.test.tsx +++ b/src/components/tools/ToolsTable.test.tsx @@ -170,7 +170,7 @@ describe("ToolsTable", () => { const tools = [createMockTool(1)]; render(); - const copyButton = screen.getByLabelText("Copy tool ID"); + const copyButton = screen.getByLabelText("Copy tool ID for Display Name 1"); expect(copyButton).toBeInTheDocument(); }); @@ -179,7 +179,7 @@ describe("ToolsTable", () => { const tools = [createMockTool(1, { id: "tool-abc-123" })]; render(); - const copyButtons = screen.getAllByLabelText("Copy tool ID"); + const copyButtons = screen.getAllByLabelText("Copy tool ID for Display Name 1"); await user.click(copyButtons[0]); expect(gatewayUtils.copyToClipboard).toHaveBeenCalledWith("tool-abc-123"); diff --git a/src/components/tools/ToolsTable.tsx b/src/components/tools/ToolsTable.tsx index 9db952e..da3685b 100644 --- a/src/components/tools/ToolsTable.tsx +++ b/src/components/tools/ToolsTable.tsx @@ -113,7 +113,10 @@ export function ToolsTable({ diff --git a/src/components/ui/code-block.test.tsx b/src/components/ui/code-block.test.tsx index 0ea98f4..0294637 100644 --- a/src/components/ui/code-block.test.tsx +++ b/src/components/ui/code-block.test.tsx @@ -10,13 +10,13 @@ describe("CodeBlock", () => { expect(screen.getByText('"a"')).toBeInTheDocument(); }); - it("copies the code to the clipboard and shows the copied tooltip when the copy button is clicked", async () => { + it("copies the code to the clipboard and announces the copied status when the copy button is clicked", async () => { const user = userEvent.setup(); const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(); render(); await user.click(screen.getByRole("button", { name: /bash/i })); expect(writeText).toHaveBeenCalledWith("hello world"); - expect(screen.getByRole("tooltip")).toHaveTextContent("Copied!"); + expect(screen.getByRole("status")).toHaveTextContent("Copied!"); }); it("hides the copy button when hideCopy is set", () => { diff --git a/src/components/ui/code-block.tsx b/src/components/ui/code-block.tsx index 0d8475f..067ec49 100644 --- a/src/components/ui/code-block.tsx +++ b/src/components/ui/code-block.tsx @@ -1,4 +1,5 @@ import { Highlight, themes, type Language } from "prism-react-renderer"; +import { useIntl } from "react-intl"; import { CopyButton } from "@/components/ui/copy-button"; import { cn } from "@/lib/utils"; @@ -8,12 +9,10 @@ export type CodeBlockLanguage = "bash" | "json" | "python" | "tsx" | "markdown" export interface CodeBlockProps { code: string; language: CodeBlockLanguage; - /** Label for the Copy button, used as both its aria-label and idle tooltip. */ + /** Accessible label for the Copy button. Defaults to a generic "Copy code". */ copyLabel?: string; /** Hide the built-in Copy affordance. */ hideCopy?: boolean; - /** aria-label fallback when `copyLabel` is not supplied. */ - copyAriaLabel?: string; className?: string; /** Pre-element padding override (defaults to p-4). */ padding?: string; @@ -45,12 +44,12 @@ export function CodeBlock({ language, copyLabel, hideCopy = false, - copyAriaLabel, className, padding = "p-4", }: CodeBlockProps) { + const intl = useIntl(); const prismLanguage = TOKEN_LANGUAGE[language]; - const ariaLabel = copyLabel ?? copyAriaLabel ?? "Copy code"; + const ariaLabel = copyLabel ?? intl.formatMessage({ id: "common.copyCode" }); return (
diff --git a/src/components/ui/copy-button.test.tsx b/src/components/ui/copy-button.test.tsx index f1b0626..c38b6f9 100644 --- a/src/components/ui/copy-button.test.tsx +++ b/src/components/ui/copy-button.test.tsx @@ -9,13 +9,30 @@ describe("CopyButton", () => { vi.clearAllMocks(); }); - it("keeps the idle label as both aria-label and hover tooltip before any click", async () => { + it("keeps aria-label fixed and shows no bubble on hover", async () => { const user = userEvent.setup(); render(); const button = screen.getByRole("button", { name: "Copy resource ID" }); await user.hover(button); - expect(await screen.findByRole("tooltip")).toHaveTextContent("Copy resource ID"); + + expect(screen.queryByText("Copy resource ID")).not.toBeInTheDocument(); + expect(screen.getByRole("status")).toHaveTextContent(""); + }); + + it("shows no bubble on focus", () => { + render(); + + const button = screen.getByRole("button", { name: "Copy resource ID" }); + button.focus(); + + expect(screen.getByRole("status")).toHaveTextContent(""); + }); + + it("mounts the status region before any copy has happened", () => { + render(); + expect(screen.getAllByRole("status")).toHaveLength(1); + expect(screen.getByRole("status")).toHaveTextContent(""); }); it("copies the value, shows the Check icon, and announces the copied label once", async () => { @@ -34,6 +51,17 @@ describe("CopyButton", () => { expect(screen.getByRole("status")).toHaveTextContent("Copied!"); }); + it("shows the confirmation bubble on click, hidden from assistive tech", async () => { + const user = userEvent.setup(); + vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(); + render(); + + await user.click(screen.getByRole("button", { name: "Copy resource ID" })); + + const bubble = screen.getByText("Copied!", { selector: "span[aria-hidden='true']" }); + expect(bubble).toBeInTheDocument(); + }); + it("stops the click from bubbling to an ancestor (e.g. a clickable table row)", async () => { const user = userEvent.setup(); vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(); diff --git a/src/components/ui/copy-button.tsx b/src/components/ui/copy-button.tsx index b393c22..e6d5b02 100644 --- a/src/components/ui/copy-button.tsx +++ b/src/components/ui/copy-button.tsx @@ -1,33 +1,34 @@ -import { useState } from "react"; +import { createPortal } from "react-dom"; import { Check, Copy } from "lucide-react"; import { useIntl } from "react-intl"; +import { useFloating, offset, flip, shift } from "@floating-ui/react-dom"; import type { VariantProps } from "class-variance-authority"; import { Button, buttonVariants } from "@/components/ui/button"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { cn } from "@/lib/utils"; export interface CopyButtonProps { /** The value written to the clipboard. */ value: string; - /** Accessible label and idle hover-tooltip text, e.g. "Copy resource ID". */ + /** Accessible label, e.g. "Copy resource ID". Not shown on hover or focus. */ label: string; size?: VariantProps["size"]; /** Icon size class. Defaults to the size most call sites use. */ iconClassName?: string; className?: string; - side?: "top" | "right" | "bottom" | "left"; } /** * Icon button that copies `value` to the clipboard and confirms it happened. * - * The tooltip stays uncontrolled on hover/focus (so the ordinary "Copy X" - * hint keeps working) but is force-opened on copy, so touch users — who never - * fire a hover event — still see the confirmation. `aria-label` stays fixed - * at `label`; a single `role="status"` region announces the transient - * copied/failed state instead, so the two don't double-announce. + * The confirmation bubble appears on click only — hover and focus show + * nothing, so it never fights a dialog's Escape handling (see + * react-dismissable-layer). It's a plain `aria-hidden` span positioned with + * `@floating-ui/react-dom` and portaled to `document.body` so it can't clip + * inside a scrolling table or dialog. `aria-label` stays fixed at `label`; + * a single always-mounted `role="status"` region is the one non-visual + * announcement channel for the transient copied/failed state. */ export function CopyButton({ value, @@ -35,46 +36,55 @@ export function CopyButton({ size = "icon-xs", iconClassName = "size-3.5", className, - side = "top", }: CopyButtonProps) { const intl = useIntl(); - const [hoverOpen, setHoverOpen] = useState(false); const { status, copy } = useCopyToClipboard(); const copied = status === "copied"; const failed = status === "error"; const copiedLabel = intl.formatMessage({ id: "common.copied" }); const failedLabel = intl.formatMessage({ id: "common.copyFailed" }); - const tooltipLabel = copied ? copiedLabel : failed ? failedLabel : label; + + const { refs, floatingStyles } = useFloating({ + placement: "top", + middleware: [offset(6), flip(), shift({ padding: 8 })], + }); return ( <> - - - + {(copied || failed) && + createPortal( + - {tooltipLabel} - - {(copied || failed) && ( - - {copied ? copiedLabel : failedLabel} - - )} + {copied ? copiedLabel : failedLabel} + , + document.body, + )} + {/* Always mounted, so the text change on copy is what gets announced. */} + + {copied ? copiedLabel : failed ? failedLabel : ""} + ); } diff --git a/src/i18n/locales/en-US/common.json b/src/i18n/locales/en-US/common.json index adc2a1e..4ea9766 100644 --- a/src/i18n/locales/en-US/common.json +++ b/src/i18n/locales/en-US/common.json @@ -11,6 +11,7 @@ "common.button.back": "Back", "common.button.submitting": "Submitting...", "common.copyValue": "Copy {label}", + "common.copyCode": "Copy code", "common.copied": "Copied!", "common.copyFailed": "Copy failed", "common.loading": "Loading...", diff --git a/src/i18n/locales/en-US/gateways.json b/src/i18n/locales/en-US/gateways.json index d958d75..8fce541 100644 --- a/src/i18n/locales/en-US/gateways.json +++ b/src/i18n/locales/en-US/gateways.json @@ -132,6 +132,7 @@ "gateways.details.lastModified": "Last modified", "gateways.details.copyLabel": "Copy {label}", "gateways.details.serverIdCopyLabel": "server ID", + "gateways.details.urlCopyLabel": "URL", "gateways.details.actionsFor": "Actions for {name}", "gateways.details.notAvailable": "N/A", "gateways.details.tagFallback": "Tag" diff --git a/src/i18n/locales/en-US/mcpServer.json b/src/i18n/locales/en-US/mcpServer.json index 0c398f3..b1a3cb8 100644 --- a/src/i18n/locales/en-US/mcpServer.json +++ b/src/i18n/locales/en-US/mcpServer.json @@ -43,6 +43,8 @@ "mcpServer.table.components.resources": "{count, plural, one {# resource} other {# resources}}", "mcpServer.table.components.prompts": "{count, plural, one {# prompt} other {# prompts}}", "mcpServer.table.copyUuid": "Copy UUID for {name}", + "mcpServer.details.uuid": "UUID", + "mcpServer.details.url": "URL", "mcpServer.table.actions.label": "Actions for {name}", "mcpServer.table.actions.openMenu": "Open menu for {name}", "mcpServer.table.actions.viewDetails": "View details", diff --git a/src/i18n/locales/en-US/resources.json b/src/i18n/locales/en-US/resources.json index e8b431a..9092264 100644 --- a/src/i18n/locales/en-US/resources.json +++ b/src/i18n/locales/en-US/resources.json @@ -57,8 +57,8 @@ "resources.table.resource": "Resource", "resources.table.uri": "URI", "resources.table.resourceId": "Resource ID", - "resources.table.copyUri": "Copy {uri}", - "resources.table.copyResourceId": "Copy resource ID", + "resources.table.copyUri": "Copy URI for {name}", + "resources.table.copyResourceId": "Copy resource ID for {name}", "resources.table.moreOptionsFor": "More options for {name}", "resources.table.edit": "Edit", "resources.table.delete": "Delete", @@ -98,6 +98,7 @@ "resources.details.preview.openInNewTab": "Open in new tab", "resources.details.preview.downloadRaw": "Download raw", "resources.details.preview.imageAlt": "Previewed {mimeType} image, {size}", + "resources.details.preview.contentFallback": "content", "resources.details.label.status": "Status", "resources.details.label.visibility": "Visibility", "resources.details.label.type": "Type", diff --git a/src/i18n/locales/en-US/tools.json b/src/i18n/locales/en-US/tools.json index da298c4..7dbdde5 100644 --- a/src/i18n/locales/en-US/tools.json +++ b/src/i18n/locales/en-US/tools.json @@ -21,8 +21,10 @@ "tools.table.toolId": "Tool ID", "tools.table.schema": "Schema", "tools.table.copyName": "Copy {name}", - "tools.table.copyToolId": "Copy tool ID", + "tools.table.copyToolId": "Copy tool ID for {name}", "tools.table.viewSchema": "View schema", + "tools.schemaDialog.input": "Input", + "tools.schemaDialog.output": "Output", "tools.table.moreOptions": "More options", "tools.table.edit": "Edit", "tools.table.activate": "Activate", diff --git a/src/i18n/locales/es-ES/common.json b/src/i18n/locales/es-ES/common.json index 20d4251..a698c6a 100644 --- a/src/i18n/locales/es-ES/common.json +++ b/src/i18n/locales/es-ES/common.json @@ -11,6 +11,7 @@ "common.button.back": "Volver", "common.button.submitting": "Enviando...", "common.copyValue": "Copiar {label}", + "common.copyCode": "Copiar código", "common.copied": "¡Copiado!", "common.copyFailed": "Error al copiar", "common.loading": "Cargando...", diff --git a/src/i18n/locales/es-ES/gateways.json b/src/i18n/locales/es-ES/gateways.json index 1ed9476..3483a50 100644 --- a/src/i18n/locales/es-ES/gateways.json +++ b/src/i18n/locales/es-ES/gateways.json @@ -132,6 +132,7 @@ "gateways.details.lastModified": "Última modificación", "gateways.details.copyLabel": "Copiar {label}", "gateways.details.serverIdCopyLabel": "ID del servidor", + "gateways.details.urlCopyLabel": "URL", "gateways.details.actionsFor": "Acciones para {name}", "gateways.details.notAvailable": "N/D", "gateways.details.tagFallback": "Etiqueta" diff --git a/src/i18n/locales/es-ES/mcpServer.json b/src/i18n/locales/es-ES/mcpServer.json index 28b9edb..b0b6778 100644 --- a/src/i18n/locales/es-ES/mcpServer.json +++ b/src/i18n/locales/es-ES/mcpServer.json @@ -43,6 +43,8 @@ "mcpServer.table.components.resources": "{count, plural, one {# recurso} other {# recursos}}", "mcpServer.table.components.prompts": "{count, plural, one {# prompt} other {# prompts}}", "mcpServer.table.copyUuid": "Copiar UUID de {name}", + "mcpServer.details.uuid": "UUID", + "mcpServer.details.url": "URL", "mcpServer.table.actions.label": "Acciones para {name}", "mcpServer.table.actions.openMenu": "Abrir menú de {name}", "mcpServer.table.actions.viewDetails": "Ver detalles", diff --git a/src/i18n/locales/es-ES/resources.json b/src/i18n/locales/es-ES/resources.json index e404905..dcc34df 100644 --- a/src/i18n/locales/es-ES/resources.json +++ b/src/i18n/locales/es-ES/resources.json @@ -57,8 +57,8 @@ "resources.table.resource": "Recurso", "resources.table.uri": "URI", "resources.table.resourceId": "ID del recurso", - "resources.table.copyUri": "Copiar {uri}", - "resources.table.copyResourceId": "Copiar ID del recurso", + "resources.table.copyUri": "Copiar URI de {name}", + "resources.table.copyResourceId": "Copiar ID del recurso de {name}", "resources.table.moreOptionsFor": "Más opciones para {name}", "resources.table.edit": "Editar", "resources.table.delete": "Eliminar", @@ -98,6 +98,7 @@ "resources.details.preview.openInNewTab": "Abrir en una pestaña nueva", "resources.details.preview.downloadRaw": "Descargar sin procesar", "resources.details.preview.imageAlt": "Imagen {mimeType} obtenida en la vista previa, {size}", + "resources.details.preview.contentFallback": "contenido", "resources.details.label.status": "Estado", "resources.details.label.visibility": "Visibilidad", "resources.details.label.type": "Tipo", diff --git a/src/i18n/locales/es-ES/tools.json b/src/i18n/locales/es-ES/tools.json index 652aba0..46ed52c 100644 --- a/src/i18n/locales/es-ES/tools.json +++ b/src/i18n/locales/es-ES/tools.json @@ -21,8 +21,10 @@ "tools.table.toolId": "ID de herramienta", "tools.table.schema": "Esquema", "tools.table.copyName": "Copiar {name}", - "tools.table.copyToolId": "Copiar ID de herramienta", + "tools.table.copyToolId": "Copiar ID de herramienta de {name}", "tools.table.viewSchema": "Ver esquema", + "tools.schemaDialog.input": "Entrada", + "tools.schemaDialog.output": "Salida", "tools.table.moreOptions": "Más opciones", "tools.table.edit": "Editar", "tools.table.activate": "Activar", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 55dcda2..2840541 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -11,6 +11,7 @@ "common.button.back": "Voltar", "common.button.submitting": "Enviando...", "common.copyValue": "Copiar {label}", + "common.copyCode": "Copiar código", "common.copied": "Copiado!", "common.copyFailed": "Falha ao copiar", "common.loading": "Carregando...", diff --git a/src/i18n/locales/pt-BR/gateways.json b/src/i18n/locales/pt-BR/gateways.json index bc3d130..4956fa6 100644 --- a/src/i18n/locales/pt-BR/gateways.json +++ b/src/i18n/locales/pt-BR/gateways.json @@ -132,6 +132,7 @@ "gateways.details.lastModified": "Última modificação", "gateways.details.copyLabel": "Copiar {label}", "gateways.details.serverIdCopyLabel": "ID do servidor", + "gateways.details.urlCopyLabel": "URL", "gateways.details.actionsFor": "Ações para {name}", "gateways.details.notAvailable": "N/D", "gateways.details.tagFallback": "Tag" diff --git a/src/i18n/locales/pt-BR/mcpServer.json b/src/i18n/locales/pt-BR/mcpServer.json index 0555bdf..71fa9e3 100644 --- a/src/i18n/locales/pt-BR/mcpServer.json +++ b/src/i18n/locales/pt-BR/mcpServer.json @@ -43,6 +43,8 @@ "mcpServer.table.components.resources": "{count, plural, one {# recurso} other {# recursos}}", "mcpServer.table.components.prompts": "{count, plural, one {# prompt} other {# prompts}}", "mcpServer.table.copyUuid": "Copiar UUID de {name}", + "mcpServer.details.uuid": "UUID", + "mcpServer.details.url": "URL", "mcpServer.table.actions.label": "Ações para {name}", "mcpServer.table.actions.openMenu": "Abrir menu de {name}", "mcpServer.table.actions.viewDetails": "Ver detalhes", diff --git a/src/i18n/locales/pt-BR/resources.json b/src/i18n/locales/pt-BR/resources.json index 875e9e8..2de4eb4 100644 --- a/src/i18n/locales/pt-BR/resources.json +++ b/src/i18n/locales/pt-BR/resources.json @@ -57,8 +57,8 @@ "resources.table.resource": "Recurso", "resources.table.uri": "URI", "resources.table.resourceId": "ID do recurso", - "resources.table.copyUri": "Copiar {uri}", - "resources.table.copyResourceId": "Copiar ID do recurso", + "resources.table.copyUri": "Copiar URI de {name}", + "resources.table.copyResourceId": "Copiar ID do recurso de {name}", "resources.table.moreOptionsFor": "Mais opções para {name}", "resources.table.edit": "Editar", "resources.table.delete": "Excluir", @@ -98,6 +98,7 @@ "resources.details.preview.openInNewTab": "Abrir em nova aba", "resources.details.preview.downloadRaw": "Baixar bruto", "resources.details.preview.imageAlt": "Imagem {mimeType} da pré-visualização, {size}", + "resources.details.preview.contentFallback": "conteúdo", "resources.details.label.status": "Status", "resources.details.label.visibility": "Visibilidade", "resources.details.label.type": "Tipo", diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 06b9819..59bbf4d 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -21,8 +21,10 @@ "tools.table.toolId": "ID da ferramenta", "tools.table.schema": "Esquema", "tools.table.copyName": "Copiar {name}", - "tools.table.copyToolId": "Copiar ID da ferramenta", + "tools.table.copyToolId": "Copiar ID da ferramenta de {name}", "tools.table.viewSchema": "Ver esquema", + "tools.schemaDialog.input": "Entrada", + "tools.schemaDialog.output": "Saída", "tools.table.moreOptions": "Mais opções", "tools.table.edit": "Editar", "tools.table.activate": "Ativar", From cab80692e2d1ca1d1bb566619ae9d6a955d78580 Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Mon, 24 Aug 2026 17:48:24 +0100 Subject: [PATCH 2/2] fix: address comments Signed-off-by: Marek Dano --- .../gateways/VirtualServerDetailsPanel.test.tsx | 2 +- .../gateways/VirtualServerDetailsPanel.tsx | 4 ++-- .../servers/MCPServerDetailsPanel.test.tsx | 10 +++++----- src/components/servers/MCPServerDetailsPanel.tsx | 16 ++++++++++++---- src/i18n/locales/en-US/gateways.json | 3 +++ src/i18n/locales/en-US/mcpServer.json | 6 ++++++ src/i18n/locales/es-ES/gateways.json | 3 +++ src/i18n/locales/es-ES/mcpServer.json | 6 ++++++ src/i18n/locales/pt-BR/gateways.json | 3 +++ src/i18n/locales/pt-BR/mcpServer.json | 6 ++++++ 10 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/components/gateways/VirtualServerDetailsPanel.test.tsx b/src/components/gateways/VirtualServerDetailsPanel.test.tsx index b88be95..afbe873 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.test.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.test.tsx @@ -140,7 +140,7 @@ describe("VirtualServerDetailsPanel components list", () => { await screen.findByText("Titled Tool"); - await user.click(screen.getByRole("button", { name: "Copy Titled Tool" })); + await user.click(screen.getByRole("button", { name: "Copy tool name for Titled Tool" })); expect(copyToClipboard).toHaveBeenCalledWith("titled-tool-id"); // The untitled row has no separate name to reference, so its copy label diff --git a/src/components/gateways/VirtualServerDetailsPanel.tsx b/src/components/gateways/VirtualServerDetailsPanel.tsx index 5ed1ce6..ce982c6 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.tsx @@ -588,8 +588,8 @@ export function VirtualServerDetailsPanel({ diff --git a/src/components/servers/MCPServerDetailsPanel.test.tsx b/src/components/servers/MCPServerDetailsPanel.test.tsx index 68b6383..7d7310c 100644 --- a/src/components/servers/MCPServerDetailsPanel.test.tsx +++ b/src/components/servers/MCPServerDetailsPanel.test.tsx @@ -817,9 +817,9 @@ describe("MCPServerDetailsPanel", () => { />, ); - // A titled tool copies via its title label; an untitled one via the - // component-type noun (its identifier is a raw value, not a name). - await user.click(await screen.findByRole("button", { name: "Copy Tool One" })); + // A titled tool copies via a "tool name for {title}" label; an untitled + // one via the component-type noun (its identifier is a raw value, not a name). + await user.click(await screen.findByRole("button", { name: "Copy tool name for Tool One" })); expect(copyToClipboard).toHaveBeenCalledWith("original_tool_1"); await user.click(screen.getByRole("button", { name: "Copy tool" })); @@ -837,7 +837,7 @@ describe("MCPServerDetailsPanel", () => { initialTab="components" />, ); - await screen.findByRole("button", { name: "Copy Tool One" }); + await screen.findByRole("button", { name: "Copy tool name for Tool One" }); const allTab = screen.getByRole("tab", { name: "All" }); allTab.focus(); @@ -927,7 +927,7 @@ describe("MCPServerDetailsPanel", () => { initialTab="components" />, ); - await screen.findByRole("button", { name: "Copy Tool One" }); + await screen.findByRole("button", { name: "Copy tool name for Tool One" }); const searchBox = screen.getByRole("searchbox"); await user.click(searchBox); diff --git a/src/components/servers/MCPServerDetailsPanel.tsx b/src/components/servers/MCPServerDetailsPanel.tsx index 6769d9e..7ad1758 100644 --- a/src/components/servers/MCPServerDetailsPanel.tsx +++ b/src/components/servers/MCPServerDetailsPanel.tsx @@ -265,6 +265,12 @@ export function MCPServerDetailsPanel({ const intl = useIntl(); + const getComponentLabel = useCallback( + (type: Exclude) => + intl.formatMessage({ id: `mcpServer.details.component.${type}` }), + [intl], + ); + const getVisibilityLabel = useCallback( (value?: string) => { if (value === "team") return intl.formatMessage({ id: "common.visibility.team" }); @@ -479,7 +485,7 @@ export function MCPServerDetailsPanel({ {getComponentIcon(component.type)} - {component.type.slice(0, -1)} + {getComponentLabel(component.type)} {title ? ( <> @@ -491,8 +497,10 @@ export function MCPServerDetailsPanel({ @@ -506,7 +514,7 @@ export function MCPServerDetailsPanel({ value={identifier} label={intl.formatMessage( { id: "common.copyValue" }, - { label: component.type.slice(0, -1) }, + { label: getComponentLabel(component.type) }, )} className="size-5 text-muted-foreground" /> diff --git a/src/i18n/locales/en-US/gateways.json b/src/i18n/locales/en-US/gateways.json index 8fce541..e2087ce 100644 --- a/src/i18n/locales/en-US/gateways.json +++ b/src/i18n/locales/en-US/gateways.json @@ -113,6 +113,9 @@ "gateways.details.component.tools": "tool", "gateways.details.component.resources": "resource", "gateways.details.component.prompts": "prompt", + "gateways.details.component.copyName.tools": "Copy tool name for {name}", + "gateways.details.component.copyName.resources": "Copy URI for {name}", + "gateways.details.component.copyName.prompts": "Copy prompt name for {name}", "gateways.details.noComponentsFound": "No components found.", "gateways.details.noFilteredComponentsFound": "No {filter} found.", "gateways.details.close": "Close virtual server details", diff --git a/src/i18n/locales/en-US/mcpServer.json b/src/i18n/locales/en-US/mcpServer.json index b1a3cb8..0eaa16d 100644 --- a/src/i18n/locales/en-US/mcpServer.json +++ b/src/i18n/locales/en-US/mcpServer.json @@ -45,6 +45,12 @@ "mcpServer.table.copyUuid": "Copy UUID for {name}", "mcpServer.details.uuid": "UUID", "mcpServer.details.url": "URL", + "mcpServer.details.component.tools": "tool", + "mcpServer.details.component.resources": "resource", + "mcpServer.details.component.prompts": "prompt", + "mcpServer.details.component.copyName.tools": "Copy tool name for {name}", + "mcpServer.details.component.copyName.resources": "Copy URI for {name}", + "mcpServer.details.component.copyName.prompts": "Copy prompt name for {name}", "mcpServer.table.actions.label": "Actions for {name}", "mcpServer.table.actions.openMenu": "Open menu for {name}", "mcpServer.table.actions.viewDetails": "View details", diff --git a/src/i18n/locales/es-ES/gateways.json b/src/i18n/locales/es-ES/gateways.json index 3483a50..f17825a 100644 --- a/src/i18n/locales/es-ES/gateways.json +++ b/src/i18n/locales/es-ES/gateways.json @@ -113,6 +113,9 @@ "gateways.details.component.tools": "herramienta", "gateways.details.component.resources": "recurso", "gateways.details.component.prompts": "prompt", + "gateways.details.component.copyName.tools": "Copiar nombre de la herramienta de {name}", + "gateways.details.component.copyName.resources": "Copiar URI de {name}", + "gateways.details.component.copyName.prompts": "Copiar nombre del prompt de {name}", "gateways.details.noComponentsFound": "No se encontraron componentes.", "gateways.details.noFilteredComponentsFound": "No se encontraron {filter}.", "gateways.details.close": "Cerrar detalles del servidor virtual", diff --git a/src/i18n/locales/es-ES/mcpServer.json b/src/i18n/locales/es-ES/mcpServer.json index b0b6778..adaeccb 100644 --- a/src/i18n/locales/es-ES/mcpServer.json +++ b/src/i18n/locales/es-ES/mcpServer.json @@ -45,6 +45,12 @@ "mcpServer.table.copyUuid": "Copiar UUID de {name}", "mcpServer.details.uuid": "UUID", "mcpServer.details.url": "URL", + "mcpServer.details.component.tools": "herramienta", + "mcpServer.details.component.resources": "recurso", + "mcpServer.details.component.prompts": "prompt", + "mcpServer.details.component.copyName.tools": "Copiar nombre de la herramienta de {name}", + "mcpServer.details.component.copyName.resources": "Copiar URI de {name}", + "mcpServer.details.component.copyName.prompts": "Copiar nombre del prompt de {name}", "mcpServer.table.actions.label": "Acciones para {name}", "mcpServer.table.actions.openMenu": "Abrir menú de {name}", "mcpServer.table.actions.viewDetails": "Ver detalles", diff --git a/src/i18n/locales/pt-BR/gateways.json b/src/i18n/locales/pt-BR/gateways.json index 4956fa6..ababdcf 100644 --- a/src/i18n/locales/pt-BR/gateways.json +++ b/src/i18n/locales/pt-BR/gateways.json @@ -113,6 +113,9 @@ "gateways.details.component.tools": "ferramenta", "gateways.details.component.resources": "recurso", "gateways.details.component.prompts": "prompt", + "gateways.details.component.copyName.tools": "Copiar nome da ferramenta de {name}", + "gateways.details.component.copyName.resources": "Copiar URI de {name}", + "gateways.details.component.copyName.prompts": "Copiar nome do prompt de {name}", "gateways.details.noComponentsFound": "Nenhum componente encontrado.", "gateways.details.noFilteredComponentsFound": "Nenhum {filter} encontrado.", "gateways.details.close": "Fechar detalhes do servidor virtual", diff --git a/src/i18n/locales/pt-BR/mcpServer.json b/src/i18n/locales/pt-BR/mcpServer.json index 71fa9e3..e6a1fac 100644 --- a/src/i18n/locales/pt-BR/mcpServer.json +++ b/src/i18n/locales/pt-BR/mcpServer.json @@ -45,6 +45,12 @@ "mcpServer.table.copyUuid": "Copiar UUID de {name}", "mcpServer.details.uuid": "UUID", "mcpServer.details.url": "URL", + "mcpServer.details.component.tools": "ferramenta", + "mcpServer.details.component.resources": "recurso", + "mcpServer.details.component.prompts": "prompt", + "mcpServer.details.component.copyName.tools": "Copiar nome da ferramenta de {name}", + "mcpServer.details.component.copyName.resources": "Copiar URI de {name}", + "mcpServer.details.component.copyName.prompts": "Copiar nome do prompt de {name}", "mcpServer.table.actions.label": "Ações para {name}", "mcpServer.table.actions.openMenu": "Abrir menu de {name}", "mcpServer.table.actions.viewDetails": "Ver detalhes",