Add clipboard copy functionality that works over HTTP (#4446)

This commit is contained in:
Shuchang Zheng
2026-01-14 11:53:12 -08:00
committed by GitHub
parent e617ef9924
commit fed12acfc9
6 changed files with 73 additions and 39 deletions

View File

@@ -37,6 +37,7 @@ import {
newWssBaseUrl,
getRuntimeApiKey,
} from "@/util/env";
import { copyText } from "@/util/copyText";
import { cn } from "@/util/utils";
import { RotateThrough } from "./RotateThrough";
@@ -698,14 +699,21 @@ function BrowserStream({
case "copied-text": {
const text = message.text;
navigator.clipboard
.writeText(text)
.then(() => {
toast({
title: "Copied to Clipboard",
description:
"The text has been copied to your clipboard. NOTE: copy-paste only works in the web page - not in the browser (like the address bar).",
});
copyText(text)
.then((success) => {
if (success) {
toast({
title: "Copied to Clipboard",
description:
"The text has been copied to your clipboard. NOTE: copy-paste only works in the web page - not in the browser (like the address bar).",
});
} else {
toast({
variant: "destructive",
title: "Failed to write to Clipboard",
description: "The text could not be copied to your clipboard.",
});
}
})
.catch((err) => {
console.error("Failed to write to clipboard:", err);

View File

@@ -2,15 +2,16 @@ import { useState } from "react";
import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
import { Button } from "@/components/ui/button";
import { copyText } from "@/util/copyText";
function CopyButton({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
const handleCopy = async () => {
if (copied) {
return;
}
window.navigator.clipboard.writeText(value);
await copyText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};

View File

@@ -73,6 +73,7 @@ import {
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
import { DebuggerBlockRuns } from "@/routes/workflows/debugger/DebuggerBlockRuns";
import { copyText } from "@/util/copyText";
import { cn } from "@/util/utils";
import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
@@ -196,8 +197,8 @@ function CopyAndExplainCode({ code }: { code: string }) {
function CopyText({ className, text }: { className?: string; text: string }) {
const [wasCopied, setWasCopied] = useState(false);
function handleCopy(code: string) {
navigator.clipboard.writeText(code);
async function handleCopy(code: string) {
await copyText(code);
setWasCopied(true);
setTimeout(() => setWasCopied(false), 2000);
}

View File

@@ -21,6 +21,7 @@ import {
} from "@radix-ui/react-icons";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { copyText } from "@/util/copyText";
type Props = {
onImport: (data: {
@@ -156,12 +157,20 @@ export function CurlImportDialog({ onImport, children }: Props) {
}
};
const copyExample = (example: string) => {
navigator.clipboard.writeText(example);
toast({
title: "Copied",
description: "Example copied to clipboard",
});
const copyExample = async (example: string) => {
const success = await copyText(example);
if (success) {
toast({
title: "Copied",
description: "Example copied to clipboard",
});
} else {
toast({
title: "Error",
description: "Failed to copy example",
variant: "destructive",
});
}
};
return (

View File

@@ -8,6 +8,7 @@ import {
} from "@radix-ui/react-icons";
import { useState } from "react";
import { toast } from "@/components/ui/use-toast";
import { copyText } from "@/util/copyText";
import { cn } from "@/util/utils";
// HTTP Method Badge Component
@@ -140,16 +141,16 @@ export function CopyToCurlButton({
};
const handleCopy = async () => {
try {
const curlCommand = generateCurlCommand();
await navigator.clipboard.writeText(curlCommand);
const curlCommand = generateCurlCommand();
const success = await copyText(curlCommand);
if (success) {
setCopied(true);
toast({
title: "Copied!",
description: "cURL command copied to clipboard",
});
setTimeout(() => setCopied(false), 2000);
} catch (error) {
} else {
toast({
title: "Error",
description: "Failed to copy cURL command",

View File

@@ -1,25 +1,39 @@
/**
* Progressively enhanced text copying
* Progressively enhanced text copying with HTTP fallback
* https://web.dev/patterns/clipboard/copy-text
*
* Uses navigator.clipboard when in a secure context (HTTPS),
* falls back to textarea + execCommand for HTTP contexts.
*/
async function copyText(text: string): Promise<void> {
if ("clipboard" in navigator) {
return navigator.clipboard.writeText(text);
} else {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const success = document.execCommand("copy");
document.body.removeChild(textArea);
if (success) {
return Promise.resolve();
} else {
return Promise.reject();
async function copyText(text: string): Promise<boolean> {
// Prefer navigator.clipboard when in a secure context
if (window.isSecureContext && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
// Fall through to fallback
}
}
// Fallback for HTTP or when clipboard API fails
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
textArea.style.left = "-9999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
// execCommand is deprecated but remains the only option for HTTP contexts
// where navigator.clipboard is unavailable. Browser support remains strong.
const success = document.execCommand("copy");
return success;
} finally {
document.body.removeChild(textArea);
}
}
export { copyText };