From 9c796edbde33b7c2ec71dc86875f9ba2639ba86d Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 2 Sep 2026 08:34:52 -0400 Subject: [PATCH] refactor(client): drop the copy button, keep the selection fix The selection guard alone closes #3907: drag-selecting a check's error message no longer navigates away, so the text can be copied. The copy button was a second, larger answer to the same problem, and it brought a reusable component, an HTTP clipboard fallback, four translation keys and a timer to maintain. Reverting it leaves the fix at ~29 lines in one file, and keeps the behaviour change to the thing that was actually broken. CopyButton is worth having for monitor URLs, tokens and incident ids, but it should land on its own merits rather than inside a bug fix. Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd --- .../Components/design-elements/CopyButton.tsx | 94 ------------------- .../design-elements/CopyableText.tsx | 41 -------- .../src/Components/design-elements/index.tsx | 2 - .../Pages/Checks/Components/ChecksTable.tsx | 3 +- .../Uptime/Details/Components/ChecksTable.tsx | 3 +- client/src/locales/en.json | 4 - 6 files changed, 2 insertions(+), 145 deletions(-) delete mode 100644 client/src/Components/design-elements/CopyButton.tsx delete mode 100644 client/src/Components/design-elements/CopyableText.tsx diff --git a/client/src/Components/design-elements/CopyButton.tsx b/client/src/Components/design-elements/CopyButton.tsx deleted file mode 100644 index 5d45b9739..000000000 --- a/client/src/Components/design-elements/CopyButton.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import IconButton from "@mui/material/IconButton"; -import Tooltip from "@mui/material/Tooltip"; -import { useTheme } from "@mui/material/styles"; -import { Check, Copy } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { useToast } from "@/Hooks/UseToast"; - -const RESET_DELAY_MS = 1500; - -/** - * `navigator.clipboard` only exists on secure origins, and Checkmate is often - * self-hosted over plain HTTP on a LAN address. Fall back to a hidden textarea - * there rather than leaving the button permanently broken. - */ -const copyText = async (value: string) => { - if (navigator.clipboard?.writeText) { - return navigator.clipboard.writeText(value); - } - - const textarea = document.createElement("textarea"); - textarea.value = value; - textarea.setAttribute("readonly", ""); - textarea.style.position = "fixed"; - textarea.style.opacity = "0"; - document.body.appendChild(textarea); - textarea.select(); - - try { - if (!document.execCommand("copy")) throw new Error("Copy command was rejected"); - } finally { - document.body.removeChild(textarea); - } -}; - -/** - * Copies `value` to the clipboard. Shows a tick for a moment so the click is - * acknowledged without moving the user anywhere. - */ -const CopyButton = ({ value, label }: { value: string; label?: string }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const { toastError } = useToast(); - const [copied, setCopied] = useState(false); - const resetTimer = useRef>(undefined); - - // These rows unmount on pagination, so the tick timer has to be cancelled. - useEffect(() => () => clearTimeout(resetTimer.current), []); - - const handleCopy = async (e: React.MouseEvent) => { - // The button usually sits inside a clickable row. - e.stopPropagation(); - - try { - await copyText(value); - setCopied(true); - clearTimeout(resetTimer.current); - resetTimer.current = setTimeout(() => setCopied(false), RESET_DELAY_MS); - } catch { - toastError(t("common.copyFailed")); - } - }; - - const title = copied ? t("common.copied") : (label ?? t("common.copy")); - - return ( - - - {copied ? ( - - ) : ( - - )} - - - ); -}; - -export default CopyButton; diff --git a/client/src/Components/design-elements/CopyableText.tsx b/client/src/Components/design-elements/CopyableText.tsx deleted file mode 100644 index 5d09e307b..000000000 --- a/client/src/Components/design-elements/CopyableText.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import Stack from "@mui/material/Stack"; -import { useTheme } from "@mui/material/styles"; -import { useTranslation } from "react-i18next"; -import { LAYOUT } from "@/Utils/Theme/constants"; -import CopyButton from "./CopyButton"; - -/** - * Text alongside a copy button, for table cells holding values worth lifting - * out - error messages in particular. - */ -const CopyableText = ({ - value, - fallback = "N/A", - copyLabel, -}: { - value?: string | null; - fallback?: string; - copyLabel?: string; -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - if (!value) return <>{fallback}; - - return ( - - {value} - - - ); -}; - -export default CopyableText; diff --git a/client/src/Components/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx index e1c1c3965..d7d38d014 100644 --- a/client/src/Components/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -28,5 +28,3 @@ export * from "./Avatar"; export * from "./StatusCodeLabel"; export * from "./StrategyBadge"; export * from "./StepProgress"; -export { default as CopyButton } from "./CopyButton"; -export { default as CopyableText } from "./CopyableText"; diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx index 4d5eebbdf..31737f4bb 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -3,7 +3,6 @@ import { Pagination, StatusLabel, StatusCodeLabel, - CopyableText, } from "@/Components/design-elements"; import Box from "@mui/material/Box"; import type { Header } from "@/Components/design-elements/Table"; @@ -80,7 +79,7 @@ export const ChecksTable = ({ id: "message", content: t("common.table.headers.message"), render: (row) => { - return ; + return row.message || "N/A"; }, }, ]; diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx index ae1d87d45..b100c28c4 100644 --- a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx +++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx @@ -3,7 +3,6 @@ import { Pagination, StatusLabel, StatusCodeLabel, - CopyableText, } from "@/Components/design-elements"; import Box from "@mui/material/Box"; import type { Header } from "@/Components/design-elements"; @@ -53,7 +52,7 @@ export const ChecksTable = ({ id: "message", content: t("common.table.headers.message"), render: (row) => { - return ; + return row.message || "N/A"; }, }, { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 9b193ef1f..cbebadded 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -14,10 +14,6 @@ } }, "appName": "Checkmate", - "copy": "Copy", - "copied": "Copied", - "copyMessage": "Copy message", - "copyFailed": "Could not copy to clipboard", "statusCode": { "down": "Down", "networkError": {