mirror of
https://github.com/tale/headplane.git
synced 2026-10-25 14:10:43 +00:00
fix: fall back to copying via execCommand in http
This commit is contained in:
@@ -8,6 +8,7 @@ Building Headplane from source now requires Go 1.27.1 or newer.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed copying code, attributes, and machine addresses over plain HTTP, and added feedback when copying fails (closes [#597](https://github.com/tale/headplane/issues/597)).
|
||||
- Updated NPM & Go dependencies to fix vulnerabilities.
|
||||
- Fixed the Native installation documentation as it was outdated and missing steps (via [#633](https://github.com/tale/headplane/pull/633)).
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Check, Copy, Info } from "lucide-react";
|
||||
|
||||
import cn from "~/utils/cn";
|
||||
import { copyToClipboard } from "~/utils/copy";
|
||||
import toast from "~/utils/toast";
|
||||
|
||||
import Tooltip from "./tooltip";
|
||||
@@ -47,11 +48,16 @@ export default function Attribute({ name, value, tooltip, isCopyable }: Attribut
|
||||
className="relative flex w-full min-w-0 items-center gap-1.5"
|
||||
onClick={async (event) => {
|
||||
const svgs = event.currentTarget.querySelectorAll("svg");
|
||||
const isCopied = await copyToClipboard(value);
|
||||
if (!isCopied) {
|
||||
toast("Copy failed. Please copy the text manually.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const svg of svgs) {
|
||||
svg.toggleAttribute("data-copied", true);
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast(`Copied ${name} to clipboard`);
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Copy } from "lucide-react";
|
||||
|
||||
import cn from "~/utils/cn";
|
||||
import { copyToClipboard } from "~/utils/copy";
|
||||
import toast from "~/utils/toast";
|
||||
|
||||
export interface CodeBlockProps {
|
||||
@@ -20,8 +21,8 @@ export default function CodeBlock({ children, className }: CodeBlockProps) {
|
||||
className,
|
||||
)}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast("Copied to clipboard");
|
||||
const isCopied = await copyToClipboard(text);
|
||||
toast(isCopied ? "Copied to clipboard" : "Copy failed. Please copy the text manually.");
|
||||
}}
|
||||
>
|
||||
<code className="block px-3 pt-2 pb-1 text-sm break-all">{text}</code>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { SubnetTag } from "~/components/tags/Subnet";
|
||||
import { TailscaleSSHTag } from "~/components/tags/TailscaleSSH";
|
||||
import type { User } from "~/types";
|
||||
import cn from "~/utils/cn";
|
||||
import { copyToClipboard } from "~/utils/copy";
|
||||
import * as hinfo from "~/utils/host-info";
|
||||
import { isNoExpiry, type PopulatedNode } from "~/utils/node-info";
|
||||
import { formatTimeDelta } from "~/utils/time";
|
||||
@@ -89,8 +90,12 @@ export default function MachineRow({
|
||||
<MenuItem
|
||||
key={ip}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(ip);
|
||||
toast("Copied IP address to clipboard");
|
||||
const isCopied = await copyToClipboard(ip);
|
||||
toast(
|
||||
isCopied
|
||||
? "Copied IP address to clipboard"
|
||||
: "Copy failed. Please copy the address manually.",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Clipboard permissions can fail even when the API is available.
|
||||
}
|
||||
|
||||
if (typeof document === "undefined" || typeof document.execCommand !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeElement = document.activeElement;
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.readOnly = true;
|
||||
textarea.style.cssText = "position: fixed; top: 0; left: -9999px;";
|
||||
|
||||
// Keep the temporary field inside any active modal's focus trap.
|
||||
const container = activeElement?.closest('[role="dialog"], [role="alertdialog"], dialog');
|
||||
|
||||
try {
|
||||
(container ?? document.body).appendChild(textarea);
|
||||
textarea.focus({ preventScroll: true });
|
||||
textarea.select();
|
||||
return document.execCommand("copy");
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
textarea.remove();
|
||||
if (activeElement instanceof HTMLElement) {
|
||||
activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user