From 0e9cdaef7d951213f63e5aac21c25243d4be27a2 Mon Sep 17 00:00:00 2001 From: vedayeminedi Date: Fri, 24 Jul 2026 18:52:13 +0530 Subject: [PATCH] feat: add copy feedback to preferences sheet --- frontend/src/components/SettingsPanel.jsx | 30 +++++++++----- .../src/components/SettingsPanel.test.jsx | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/SettingsPanel.jsx b/frontend/src/components/SettingsPanel.jsx index 0a685e9d..ba08bbad 100644 --- a/frontend/src/components/SettingsPanel.jsx +++ b/frontend/src/components/SettingsPanel.jsx @@ -33,7 +33,7 @@ export default function SettingsPanel({ settings = {}, isLoading = false, onSave localStorage.setItem("localmind_settings_expanded", JSON.stringify(isExpanded)); }, [isExpanded]); - const [copied, setCopied] = useState(false); + const [copyStatus, setCopyStatus] = useState("idle"); // idle | success | error const [errors, setErrors] = useState({}); const [isSaving, setIsSaving] = useState(false); @@ -106,15 +106,23 @@ export default function SettingsPanel({ settings = {}, isLoading = false, onSave }; const handleCopySummary = () => { - const summaryText = `LocalMind Configuration Summary:\n- Model: ${form.default_model}\n- Language: ${form.default_language}\n- Temperature: ${form.temperature}\n- RAG Chunks: ${form.rag_top_k}\n- Max Turns: ${form.max_history_turns}\n- Theme: ${form.theme}`; + const summaryText = `LocalMind Configuration Summary: + - Model: ${form.default_model} + - Language: ${form.default_language} + - Temperature: ${form.temperature} + - RAG Chunks: ${form.rag_top_k} + - Max Turns: ${form.max_history_turns} + - Theme: ${form.theme}`; + navigator.clipboard.writeText(summaryText) .then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); + setCopyStatus("success"); + setTimeout(() => setCopyStatus("idle"), 2000); }) - .catch((err) => { - console.error("Failed to copy settings summary: ", err); - }); + .catch(() => { + setCopyStatus("error"); + setTimeout(() => setCopyStatus("idle"), 2000); + }); }; return ( @@ -356,12 +364,14 @@ export default function SettingsPanel({ settings = {}, isLoading = false, onSave type="button" onClick={handleCopySummary} className={`text-[11px] px-3 py-1.5 rounded-lg transition font-medium border flex items-center justify-center gap-1.5 duration-200 w-full sm:w-auto mt-2 sm:mt-0 focus:outline-none focus:ring-2 focus:ring-purple-500 ${ - copied + copyStatus === "success" ? "bg-green-950/40 border-green-900/60 text-green-400" - : "border-gray-700 text-gray-400 hover:bg-gray-800" + : copyStatus === "error" + ? "bg-red-950/40 border-red-900/60 text-red-400" + : "border-gray-700 text-gray-400 hover:bg-gray-800" }`} > - {copied ? <>Copied! : Copy Config Summary} + {copyStatus === "success" ? <>Copied! : copyStatus === "error" ? <>Copy failed : Copy Config Summary} diff --git a/frontend/src/components/SettingsPanel.test.jsx b/frontend/src/components/SettingsPanel.test.jsx index 305e3596..3e827da5 100644 --- a/frontend/src/components/SettingsPanel.test.jsx +++ b/frontend/src/components/SettingsPanel.test.jsx @@ -9,6 +9,13 @@ vi.mock("./Icons", () => ({ SettingsIcon: () => , })); +Object.assign(navigator, { + clipboard: { + writeText: vi.fn(), + }, +}); + + afterEach(() => { cleanup(); localStorage.clear(); @@ -185,4 +192,37 @@ describe("SettingsPanel Accessibility Landmarks & Validation Suite (#580)", () = expect(mockOnClose).toHaveBeenCalledTimes(1); }); +}); +describe("SettingsPanel Copy Feedback Suite (#790)", () => { + test("shows success feedback after copying configuration summary", async () => { + navigator.clipboard.writeText.mockResolvedValueOnce(); + + render( + + ); + + fireEvent.click(screen.getByText("Copy Config Summary")); + + expect(await screen.findByText("Copied!")).toBeInTheDocument(); + }); + + test("shows failure feedback when copying configuration summary fails", async () => { + navigator.clipboard.writeText.mockRejectedValueOnce(new Error("Copy failed")); + + render( + + ); + + fireEvent.click(screen.getByText("Copy Config Summary")); + + expect(await screen.findByText("Copy failed")).toBeInTheDocument(); + }); }); \ No newline at end of file