Add clipboard copy functionality that works over HTTP (#4446)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user