From db4689207aa6e74b4b66c5927d98ff8006651a03 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Tue, 1 Sep 2026 22:35:46 -0400 Subject: [PATCH 1/4] fix(client): make check error messages selectable and copyable Closes #3907. Selecting the text of a failed check's error message navigated away before the selection could be copied. The mouseup that ends a drag-select lands on the table row, which fires the row's onClick. Two changes: - DataTable ignores a row click when the user has an active text selection, so drag-selecting text inside any row no longer navigates. This applies to all eleven tables that pass onRowClick, not just checks. - The message column gets an explicit copy button, which is the more discoverable of the two fixes the reporter suggested. Added as a shared CopyButton in design-elements so other columns can reuse it. Note the row click on the uptime details checks table navigates to /checks/, but that route's parameter is :monitorId. A check id never matches a monitor, so the destination page renders empty - which is the "all this information disappears" in the report. Left alone here to keep this change reviewable; it needs its own decision about what that row should link to. Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd --- .../Components/design-elements/CopyButton.tsx | 64 +++++++++++++++++++ .../src/Components/design-elements/Table.tsx | 14 +++- .../src/Components/design-elements/index.tsx | 1 + .../Pages/Checks/Components/ChecksTable.tsx | 21 +++++- .../Uptime/Details/Components/ChecksTable.tsx | 21 +++++- client/src/locales/en.json | 4 ++ 6 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 client/src/Components/design-elements/CopyButton.tsx diff --git a/client/src/Components/design-elements/CopyButton.tsx b/client/src/Components/design-elements/CopyButton.tsx new file mode 100644 index 000000000..caa317598 --- /dev/null +++ b/client/src/Components/design-elements/CopyButton.tsx @@ -0,0 +1,64 @@ +import { 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; + +/** + * 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 handleCopy = async (e: React.MouseEvent) => { + // The button usually sits inside a clickable row. + e.stopPropagation(); + + try { + await navigator.clipboard.writeText(value); + setCopied(true); + 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/Table.tsx b/client/src/Components/design-elements/Table.tsx index df0d52b16..c7bdad9f8 100644 --- a/client/src/Components/design-elements/Table.tsx +++ b/client/src/Components/design-elements/Table.tsx @@ -79,6 +79,14 @@ export function DataTable< }; const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + // Selecting text inside a row ends in a mouseup on that row, which would + // otherwise navigate away and take the text with it. Ignore the click when + // the user has actually selected something. + const hasTextSelection = () => { + const selection = window.getSelection(); + return Boolean(selection && !selection.isCollapsed && selection.toString().trim()); + }; const isInteractive = Boolean(onRowClick) || expandableRows; if (data.length === 0 || headers.length === 0) { @@ -100,7 +108,10 @@ export function DataTable< keys.push(key); return ( (onRowClick ? onRowClick(row) : null)} + onClick={() => { + if (hasTextSelection()) return; + if (onRowClick) onRowClick(row); + }} spacing={theme.spacing(LAYOUT.XS)} sx={{ borderStyle: "solid", @@ -247,6 +258,7 @@ export function DataTable< ...(getRowSx?.(row) as object), }} onClick={() => { + if (hasTextSelection()) return; if (expandableRows) handleExpand(row); else if (onRowClick) onRowClick(row); }} diff --git a/client/src/Components/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx index d7d38d014..e35d95823 100644 --- a/client/src/Components/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -28,3 +28,4 @@ export * from "./Avatar"; export * from "./StatusCodeLabel"; export * from "./StrategyBadge"; export * from "./StepProgress"; +export { default as CopyButton } from "./CopyButton"; diff --git a/client/src/Pages/Checks/Components/ChecksTable.tsx b/client/src/Pages/Checks/Components/ChecksTable.tsx index 31737f4bb..0e75116dc 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -3,8 +3,12 @@ import { Pagination, StatusLabel, StatusCodeLabel, + CopyButton, } from "@/Components/design-elements"; import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import { useTheme } from "@mui/material/styles"; +import { LAYOUT } from "@/Utils/Theme/constants"; import type { Header } from "@/Components/design-elements/Table"; import type { Monitor } from "@/Types/Monitor"; import { useTranslation } from "react-i18next"; @@ -31,6 +35,7 @@ export const ChecksTable = ({ setRowsPerPage: (rowsPerPage: number) => void; }) => { const { t } = useTranslation(); + const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const getHeaders = () => { @@ -79,7 +84,21 @@ export const ChecksTable = ({ id: "message", content: t("common.table.headers.message"), render: (row) => { - return row.message || "N/A"; + if (!row.message) return "N/A"; + return ( + + {row.message} + + + ); }, }, ]; diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx index b100c28c4..91b4ce588 100644 --- a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx +++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx @@ -3,8 +3,12 @@ import { Pagination, StatusLabel, StatusCodeLabel, + CopyButton, } from "@/Components/design-elements"; import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import { useTheme } from "@mui/material/styles"; +import { LAYOUT } from "@/Utils/Theme/constants"; import type { Header } from "@/Components/design-elements"; import type { Check } from "@/Types/Check"; @@ -31,6 +35,7 @@ export const ChecksTable = ({ }) => { const navigate = useNavigate(); const { t } = useTranslation(); + const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const headers: Header[] = [ @@ -52,7 +57,21 @@ export const ChecksTable = ({ id: "message", content: t("common.table.headers.message"), render: (row) => { - return row.message || "N/A"; + if (!row.message) return "N/A"; + return ( + + {row.message} + + + ); }, }, { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cbebadded..9b193ef1f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -14,6 +14,10 @@ } }, "appName": "Checkmate", + "copy": "Copy", + "copied": "Copied", + "copyMessage": "Copy message", + "copyFailed": "Could not copy to clipboard", "statusCode": { "down": "Down", "networkError": { From 84a1b994a147197b7e0f5e4e806c142a0c9d3f91 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Tue, 1 Sep 2026 22:40:29 -0400 Subject: [PATCH 2/4] fix(client): address review feedback on the copy-message change Four issues from code review: - The selection guard read the document-wide selection, so a leftover selection anywhere on the page could swallow the first click on any row. It now checks that the selection is inside the row that was clicked, with the event threaded through to both call sites. - CopyButton's reset timer was never cleared, so it fired against an unmounted component when a row went away on pagination, and a rapid second click could not cancel the pending reset. Held in a ref and cleared on unmount and on re-copy. - navigator.clipboard only exists on secure origins, and the README documents reaching Checkmate over plain HTTP on a LAN address. There the API is undefined, so every click showed "could not copy" with no way forward. Falls back to a hidden textarea. - The two message cells were byte-identical, so they are now one shared CopyableText component. Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd --- .../Components/design-elements/CopyButton.tsx | 36 ++++++++++++++-- .../design-elements/CopyableText.tsx | 41 +++++++++++++++++++ .../src/Components/design-elements/Table.tsx | 25 +++++++---- .../src/Components/design-elements/index.tsx | 1 + .../Pages/Checks/Components/ChecksTable.tsx | 22 +--------- .../Uptime/Details/Components/ChecksTable.tsx | 22 +--------- 6 files changed, 97 insertions(+), 50 deletions(-) create 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 index caa317598..5d45b9739 100644 --- a/client/src/Components/design-elements/CopyButton.tsx +++ b/client/src/Components/design-elements/CopyButton.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import { useTheme } from "@mui/material/styles"; @@ -8,6 +8,31 @@ 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. @@ -17,15 +42,20 @@ const CopyButton = ({ value, label }: { value: string; label?: string }) => { 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 navigator.clipboard.writeText(value); + await copyText(value); setCopied(true); - setTimeout(() => setCopied(false), RESET_DELAY_MS); + clearTimeout(resetTimer.current); + resetTimer.current = setTimeout(() => setCopied(false), RESET_DELAY_MS); } catch { toastError(t("common.copyFailed")); } diff --git a/client/src/Components/design-elements/CopyableText.tsx b/client/src/Components/design-elements/CopyableText.tsx new file mode 100644 index 000000000..5d09e307b --- /dev/null +++ b/client/src/Components/design-elements/CopyableText.tsx @@ -0,0 +1,41 @@ +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/Table.tsx b/client/src/Components/design-elements/Table.tsx index c7bdad9f8..8650f2bc6 100644 --- a/client/src/Components/design-elements/Table.tsx +++ b/client/src/Components/design-elements/Table.tsx @@ -82,10 +82,21 @@ export function DataTable< // Selecting text inside a row ends in a mouseup on that row, which would // otherwise navigate away and take the text with it. Ignore the click when - // the user has actually selected something. - const hasTextSelection = () => { + // the selection is inside the row that was clicked - a leftover selection + // elsewhere on the page must not swallow a legitimate click. + const hasTextSelectionWithin = (element: Element) => { const selection = window.getSelection(); - return Boolean(selection && !selection.isCollapsed && selection.toString().trim()); + if (!selection || selection.isCollapsed || !selection.toString().trim()) { + return false; + } + return ( + selection.anchorNode !== null && + element.contains( + selection.anchorNode.nodeType === Node.TEXT_NODE + ? selection.anchorNode.parentNode + : selection.anchorNode + ) + ); }; const isInteractive = Boolean(onRowClick) || expandableRows; @@ -108,8 +119,8 @@ export function DataTable< keys.push(key); return ( { - if (hasTextSelection()) return; + onClick={(e) => { + if (hasTextSelectionWithin(e.currentTarget)) return; if (onRowClick) onRowClick(row); }} spacing={theme.spacing(LAYOUT.XS)} @@ -257,8 +268,8 @@ export function DataTable< cursor: isInteractive ? "pointer" : "default", ...(getRowSx?.(row) as object), }} - onClick={() => { - if (hasTextSelection()) return; + onClick={(e) => { + if (hasTextSelectionWithin(e.currentTarget)) return; if (expandableRows) handleExpand(row); else if (onRowClick) onRowClick(row); }} diff --git a/client/src/Components/design-elements/index.tsx b/client/src/Components/design-elements/index.tsx index e35d95823..e1c1c3965 100644 --- a/client/src/Components/design-elements/index.tsx +++ b/client/src/Components/design-elements/index.tsx @@ -29,3 +29,4 @@ 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 0e75116dc..4d5eebbdf 100644 --- a/client/src/Pages/Checks/Components/ChecksTable.tsx +++ b/client/src/Pages/Checks/Components/ChecksTable.tsx @@ -3,12 +3,9 @@ import { Pagination, StatusLabel, StatusCodeLabel, - CopyButton, + CopyableText, } from "@/Components/design-elements"; import Box from "@mui/material/Box"; -import Stack from "@mui/material/Stack"; -import { useTheme } from "@mui/material/styles"; -import { LAYOUT } from "@/Utils/Theme/constants"; import type { Header } from "@/Components/design-elements/Table"; import type { Monitor } from "@/Types/Monitor"; import { useTranslation } from "react-i18next"; @@ -35,7 +32,6 @@ export const ChecksTable = ({ setRowsPerPage: (rowsPerPage: number) => void; }) => { const { t } = useTranslation(); - const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const getHeaders = () => { @@ -84,21 +80,7 @@ export const ChecksTable = ({ id: "message", content: t("common.table.headers.message"), render: (row) => { - if (!row.message) return "N/A"; - return ( - - {row.message} - - - ); + return ; }, }, ]; diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx index 91b4ce588..ae1d87d45 100644 --- a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx +++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx @@ -3,12 +3,9 @@ import { Pagination, StatusLabel, StatusCodeLabel, - CopyButton, + CopyableText, } from "@/Components/design-elements"; import Box from "@mui/material/Box"; -import Stack from "@mui/material/Stack"; -import { useTheme } from "@mui/material/styles"; -import { LAYOUT } from "@/Utils/Theme/constants"; import type { Header } from "@/Components/design-elements"; import type { Check } from "@/Types/Check"; @@ -35,7 +32,6 @@ export const ChecksTable = ({ }) => { const navigate = useNavigate(); const { t } = useTranslation(); - const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const headers: Header[] = [ @@ -57,21 +53,7 @@ export const ChecksTable = ({ id: "message", content: t("common.table.headers.message"), render: (row) => { - if (!row.message) return "N/A"; - return ( - - {row.message} - - - ); + return ; }, }, { From 9c796edbde33b7c2ec71dc86875f9ba2639ba86d Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Wed, 2 Sep 2026 08:34:52 -0400 Subject: [PATCH 3/4] 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": { From 7fb8bab4beeb42dcdf12decc8c3133eb64b550f7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 8 Sep 2026 10:21:44 -0700 Subject: [PATCH 4/4] purge narrative comment --- client/src/Components/design-elements/Table.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/src/Components/design-elements/Table.tsx b/client/src/Components/design-elements/Table.tsx index 8650f2bc6..bba04f350 100644 --- a/client/src/Components/design-elements/Table.tsx +++ b/client/src/Components/design-elements/Table.tsx @@ -80,10 +80,6 @@ export function DataTable< const isSmall = useMediaQuery(theme.breakpoints.down("md")); - // Selecting text inside a row ends in a mouseup on that row, which would - // otherwise navigate away and take the text with it. Ignore the click when - // the selection is inside the row that was clicked - a leftover selection - // elsewhere on the page must not swallow a legitimate click. const hasTextSelectionWithin = (element: Element) => { const selection = window.getSelection(); if (!selection || selection.isCollapsed || !selection.toString().trim()) {