Add clipboard copy functionality that works over HTTP (#4446)
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
|||||||
newWssBaseUrl,
|
newWssBaseUrl,
|
||||||
getRuntimeApiKey,
|
getRuntimeApiKey,
|
||||||
} from "@/util/env";
|
} from "@/util/env";
|
||||||
|
import { copyText } from "@/util/copyText";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
import { RotateThrough } from "./RotateThrough";
|
import { RotateThrough } from "./RotateThrough";
|
||||||
@@ -698,14 +699,21 @@ function BrowserStream({
|
|||||||
case "copied-text": {
|
case "copied-text": {
|
||||||
const text = message.text;
|
const text = message.text;
|
||||||
|
|
||||||
navigator.clipboard
|
copyText(text)
|
||||||
.writeText(text)
|
.then((success) => {
|
||||||
.then(() => {
|
if (success) {
|
||||||
toast({
|
toast({
|
||||||
title: "Copied to Clipboard",
|
title: "Copied to Clipboard",
|
||||||
description:
|
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).",
|
"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) => {
|
.catch((err) => {
|
||||||
console.error("Failed to write to clipboard:", 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 { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { copyText } from "@/util/copyText";
|
||||||
|
|
||||||
function CopyButton({ value }: { value: string }) {
|
function CopyButton({ value }: { value: string }) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = async () => {
|
||||||
if (copied) {
|
if (copied) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.navigator.clipboard.writeText(value);
|
await copyText(value);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 1500);
|
setTimeout(() => setCopied(false), 1500);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import {
|
|||||||
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
|
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
|
||||||
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
|
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
|
||||||
import { DebuggerBlockRuns } from "@/routes/workflows/debugger/DebuggerBlockRuns";
|
import { DebuggerBlockRuns } from "@/routes/workflows/debugger/DebuggerBlockRuns";
|
||||||
|
import { copyText } from "@/util/copyText";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
|
import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
|
||||||
@@ -196,8 +197,8 @@ function CopyAndExplainCode({ code }: { code: string }) {
|
|||||||
function CopyText({ className, text }: { className?: string; text: string }) {
|
function CopyText({ className, text }: { className?: string; text: string }) {
|
||||||
const [wasCopied, setWasCopied] = useState(false);
|
const [wasCopied, setWasCopied] = useState(false);
|
||||||
|
|
||||||
function handleCopy(code: string) {
|
async function handleCopy(code: string) {
|
||||||
navigator.clipboard.writeText(code);
|
await copyText(code);
|
||||||
setWasCopied(true);
|
setWasCopied(true);
|
||||||
setTimeout(() => setWasCopied(false), 2000);
|
setTimeout(() => setWasCopied(false), 2000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { copyText } from "@/util/copyText";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onImport: (data: {
|
onImport: (data: {
|
||||||
@@ -156,12 +157,20 @@ export function CurlImportDialog({ onImport, children }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyExample = (example: string) => {
|
const copyExample = async (example: string) => {
|
||||||
navigator.clipboard.writeText(example);
|
const success = await copyText(example);
|
||||||
toast({
|
if (success) {
|
||||||
title: "Copied",
|
toast({
|
||||||
description: "Example copied to clipboard",
|
title: "Copied",
|
||||||
});
|
description: "Example copied to clipboard",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to copy example",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
import { copyText } from "@/util/copyText";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
// HTTP Method Badge Component
|
// HTTP Method Badge Component
|
||||||
@@ -140,16 +141,16 @@ export function CopyToCurlButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
const curlCommand = generateCurlCommand();
|
||||||
const curlCommand = generateCurlCommand();
|
const success = await copyText(curlCommand);
|
||||||
await navigator.clipboard.writeText(curlCommand);
|
if (success) {
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
toast({
|
toast({
|
||||||
title: "Copied!",
|
title: "Copied!",
|
||||||
description: "cURL command copied to clipboard",
|
description: "cURL command copied to clipboard",
|
||||||
});
|
});
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
} catch (error) {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to copy cURL command",
|
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
|
* 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> {
|
async function copyText(text: string): Promise<boolean> {
|
||||||
if ("clipboard" in navigator) {
|
// Prefer navigator.clipboard when in a secure context
|
||||||
return navigator.clipboard.writeText(text);
|
if (window.isSecureContext && navigator.clipboard?.writeText) {
|
||||||
} else {
|
try {
|
||||||
const textArea = document.createElement("textarea");
|
await navigator.clipboard.writeText(text);
|
||||||
textArea.value = text;
|
return true;
|
||||||
textArea.style.opacity = "0";
|
} catch (err) {
|
||||||
document.body.appendChild(textArea);
|
// Fall through to fallback
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
const success = document.execCommand("copy");
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
if (success) {
|
|
||||||
return Promise.resolve();
|
|
||||||
} else {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 };
|
export { copyText };
|
||||||
|
|||||||
Reference in New Issue
Block a user