diff --git a/src/components/grid/CellViewerModal.tsx b/src/components/grid/CellViewerModal.tsx index 1cfa50a..f0b90d0 100644 --- a/src/components/grid/CellViewerModal.tsx +++ b/src/components/grid/CellViewerModal.tsx @@ -3,7 +3,7 @@ import { Check, Copy, Download, Lock, Pencil, X } from "lucide-react"; import type { editor } from "monaco-editor"; import { useEffect, useMemo, useRef, useState } from "react"; import { format } from "sql-formatter"; -import { detectMime, formatBytes } from "../../lib/blob"; +import { detectMime, formatBytes, toHex } from "../../lib/blob"; import { useThemeStore } from "../../stores/themeStore"; import { BlobViewer } from "./BlobViewer"; @@ -162,12 +162,8 @@ export function CellViewerModal({ if (!isOpen) return null; const handleCopy = async () => { - // Bytes go to the clipboard as hex rather than as `137,80,78,71`. Pasting - // a byte array into anything is useless; hex at least round-trips through - // UNHEX() (#401). - await navigator.clipboard.writeText( - bytes ? bytes.map((b) => b.toString(16).padStart(2, "0")).join("") : viewerContent, - ); + // Bytes go to the clipboard as hex rather than as `137,80,78,71` (#401). + await navigator.clipboard.writeText(bytes ? toHex(bytes) : viewerContent); setCopied(true); setTimeout(() => setCopied(false), 2000); }; diff --git a/src/components/grid/TruncatedCell.tsx b/src/components/grid/TruncatedCell.tsx index d50f93f..4c81d33 100644 --- a/src/components/grid/TruncatedCell.tsx +++ b/src/components/grid/TruncatedCell.tsx @@ -1,6 +1,6 @@ -import { Maximize2 } from "lucide-react"; +import { Check, Copy, Maximize2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { describeBlob, isBytes } from "../../lib/blob"; +import { describeBlob, isBytes, toHex } from "../../lib/blob"; import { isLongTextSqlType } from "../../lib/sql-types"; interface Props { @@ -101,6 +101,27 @@ export function TruncatedCell({ onViewFull(value === null || value === undefined ? null : formatted, columnName); }; + // Copy sits beside the expand button, so a long value — a SHOW CREATE TABLE + // statement, say — can be taken without opening the viewer first (#739). + // It copies the stored text as-is, not the viewer's reformatted version; + // bytes go as hex, the same as the viewer's Copy. + const [copied, setCopied] = useState(false); + const copyTimer = useRef | null>(null); + useEffect(() => () => { + if (copyTimer.current) clearTimeout(copyTimer.current); + }, []); + const copyValue = () => { + const text = binary ? toHex(binary) : formatted; + void navigator.clipboard.writeText(text).then(() => { + setCopied(true); + if (copyTimer.current) clearTimeout(copyTimer.current); + copyTimer.current = setTimeout(() => setCopied(false), 1500); + }, () => {}); + }; + + const iconButton = + "shrink-0 rounded p-0.5 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-[var(--color-bg-tertiary)] hover:text-[var(--color-text-primary)] focus-visible:opacity-100 group-hover:opacity-100"; + return (
{formatted}
+ {showIcon && !isNull && ( + + )} {showIcon && (