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
This commit is contained in:
gorkem-bwl committed 2026-09-03 00:24:07 -04:00
1 parent 84a1b994a1
commit 9c796edbde
6 files changed
+2 -145

No files matched your search

@@ -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<ReturnType<typeof setTimeout>>(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 (
<Tooltip
title={title}
disableInteractive
>
<IconButton
onClick={handleCopy}
aria-label={title}
size="small"
>
{copied ? (
<Check
size={14}
strokeWidth={1.5}
color={theme.palette.success.main}
/>
) : (
<Copy
size={14}
strokeWidth={1.5}
color={theme.palette.text.secondary}
/>
)}
</IconButton>
</Tooltip>
);
};
export default CopyButton;
@@ -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 (
<Stack
direction="row"
alignItems="center"
justifyContent="center"
gap={theme.spacing(LAYOUT.XS)}
>
<span>{value}</span>
<CopyButton
value={value}
label={copyLabel ?? t("common.copyMessage")}
/>
</Stack>
);
};
export default CopyableText;
@@ -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";
@@ -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 <CopyableText value={row.message} />;
return row.message || "N/A";
},
},
];
@@ -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 <CopyableText value={row.message} />;
return row.message || "N/A";
},
},
{
-4
View File
@@ -14,10 +14,6 @@
}
},
"appName": "Checkmate",
"copy": "Copy",
"copied": "Copied",
"copyMessage": "Copy message",
"copyFailed": "Could not copy to clipboard",
"statusCode": {
"down": "Down",
"networkError": {