You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@casbin.apache.org by hs...@apache.org on 2026/08/23 14:54:11 UTC

(casbin-gateway) branch master updated: feat(ui): copy a toast message with one click

This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git


The following commit(s) were added to refs/heads/master by this push:
     new 2f54fae  feat(ui): copy a toast message with one click
2f54fae is described below

commit 2f54fae1ca7def846d484117ff7996b4664af134
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Sun Aug 23 22:54:03 2026 +0800

    feat(ui): copy a toast message with one click
---
 web/src/Setting.tsx                | 11 ++++++++---
 web/src/components/shared/misc.tsx | 21 ++++++++++++++++++---
 web/src/components/ui/sonner.tsx   | 38 ++++++++++++++++++++++----------------
 3 files changed, 48 insertions(+), 22 deletions(-)

diff --git a/web/src/Setting.tsx b/web/src/Setting.tsx
index 3787c9c..6e90deb 100644
--- a/web/src/Setting.tsx
+++ b/web/src/Setting.tsx
@@ -17,6 +17,7 @@ import Sdk from "casdoor-js-sdk";
 import {toast} from "sonner";
 
 import * as Conf from "@/Conf";
+import {CopyButton} from "@/components/shared/misc";
 import type {Account, ApiResponse, ThemeAlgorithm} from "@/types";
 
 /**
@@ -102,14 +103,18 @@ export function goToLink(link: string) {
 }
 
 export function showMessage(type: "" | "success" | "error" | "info", text: string) {
+  // Backend messages are worth pasting into a bug report, and a toast is gone
+  // before it can be selected by hand, so every one carries a copy button.
+  const options = {action: <CopyButton value={text} className="text-current" />};
+
   if (type === "") {
     return;
   } else if (type === "success") {
-    toast.success(text);
+    toast.success(text, options);
   } else if (type === "error") {
-    toast.error(text);
+    toast.error(text, options);
   } else {
-    toast(text);
+    toast(text, options);
   }
 }
 
diff --git a/web/src/components/shared/misc.tsx b/web/src/components/shared/misc.tsx
index 97366e6..4d7091e 100644
--- a/web/src/components/shared/misc.tsx
+++ b/web/src/components/shared/misc.tsx
@@ -40,6 +40,18 @@ export function LabelWithTip({
   );
 }
 
+function copyBySelection(text: string) {
+  const area = document.createElement("textarea");
+  area.value = text;
+  area.style.position = "fixed";
+  area.style.opacity = "0";
+  document.body.appendChild(area);
+  area.select();
+  const copied = document.execCommand("copy");
+  area.remove();
+  return copied;
+}
+
 /** Copies text to the clipboard and confirms it in place for a moment. */
 export function CopyButton({
   value,
@@ -64,13 +76,16 @@ export function CopyButton({
   }, [copied]);
 
   const handleCopy = async() => {
+    const text = String(value ?? "");
     try {
-      await navigator.clipboard.writeText(String(value ?? ""));
+      await navigator.clipboard.writeText(text);
       setCopied(true);
+      return;
     } catch {
-      // Clipboard access is denied in some embedded contexts; failing quietly
-      // is better than a toast the reader cannot act on.
+      // The clipboard API is missing on a plain-HTTP deployment and refuses an
+      // unfocused document, so the older selection copy has to carry those.
     }
+    setCopied(copyBySelection(text));
   };
 
   return (
diff --git a/web/src/components/ui/sonner.tsx b/web/src/components/ui/sonner.tsx
index 6ed18af..30b7dd8 100644
--- a/web/src/components/ui/sonner.tsx
+++ b/web/src/components/ui/sonner.tsx
@@ -14,27 +14,33 @@
 
 import {Toaster as Sonner, type ToasterProps} from "sonner";
 
+import {TooltipProvider} from "@/components/ui/tooltip";
+
 // Sonner is themed through CSS variables rather than its own light/dark prop, so
 // it follows the `.dark` class on <html> exactly like the rest of the UI and
 // never disagrees with the page behind it.
 function Toaster({...props}: ToasterProps) {
+  // The toaster hangs outside <App/>, so the copy button on a message needs its
+  // own tooltip context.
   return (
-    <Sonner
-      className="toaster group"
-      position="top-center"
-      richColors
-      closeButton
-      toastOptions={{
-        classNames: {
-          toast:
-            "group toast group-[.toaster]:bg-popover group-[.toaster]:text-popover-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
-          description: "group-[.toast]:text-muted-foreground",
-          actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
-          cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
-        },
-      }}
-      {...props}
-    />
+    <TooltipProvider>
+      <Sonner
+        className="toaster group"
+        position="top-center"
+        richColors
+        closeButton
+        toastOptions={{
+          classNames: {
+            toast:
+              "group toast group-[.toaster]:bg-popover group-[.toaster]:text-popover-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
+            description: "group-[.toast]:text-muted-foreground",
+            actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
+            cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
+          },
+        }}
+        {...props}
+      />
+    </TooltipProvider>
   );
 }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org