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:
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";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
}
|
||||
},
|
||||
"appName": "Checkmate",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"copyMessage": "Copy message",
|
||||
"copyFailed": "Could not copy to clipboard",
|
||||
"statusCode": {
|
||||
"down": "Down",
|
||||
"networkError": {
|
||||
|
||||
Reference in new issue
Block a user