Add copy-to-clipboard button on issue detail header
Adds a copy icon button to the left of the properties panel toggle on the issue detail page. Clicking it copies a markdown representation of the issue (identifier, title, description) to the clipboard and shows a success toast. The icon briefly switches to a checkmark for visual feedback. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -9,6 +9,7 @@ import { authApi } from "../api/auth";
|
|||||||
import { projectsApi } from "../api/projects";
|
import { projectsApi } from "../api/projects";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { usePanel } from "../context/PanelContext";
|
import { usePanel } from "../context/PanelContext";
|
||||||
|
import { useToast } from "../context/ToastContext";
|
||||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb";
|
import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb";
|
||||||
@@ -36,8 +37,10 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
Activity as ActivityIcon,
|
Activity as ActivityIcon,
|
||||||
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Copy,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Hexagon,
|
Hexagon,
|
||||||
ListTree,
|
ListTree,
|
||||||
@@ -196,7 +199,9 @@ export function IssueDetail() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { pushToast } = useToast();
|
||||||
const [moreOpen, setMoreOpen] = useState(false);
|
const [moreOpen, setMoreOpen] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
const [mobilePropsOpen, setMobilePropsOpen] = useState(false);
|
const [mobilePropsOpen, setMobilePropsOpen] = useState(false);
|
||||||
const [detailTab, setDetailTab] = useState("comments");
|
const [detailTab, setDetailTab] = useState("comments");
|
||||||
const [secondaryOpen, setSecondaryOpen] = useState({
|
const [secondaryOpen, setSecondaryOpen] = useState({
|
||||||
@@ -585,6 +590,15 @@ export function IssueDetail() {
|
|||||||
return () => closePanel();
|
return () => closePanel();
|
||||||
}, [issue]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [issue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const copyIssueToClipboard = async () => {
|
||||||
|
if (!issue) return;
|
||||||
|
const md = `# ${issue.identifier}: ${issue.title}\n\n${issue.description ?? ""}`;
|
||||||
|
await navigator.clipboard.writeText(md);
|
||||||
|
setCopied(true);
|
||||||
|
pushToast({ title: "Copied to clipboard", tone: "success" });
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||||
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
||||||
if (!issue) return null;
|
if (!issue) return null;
|
||||||
@@ -737,17 +751,34 @@ export function IssueDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<div className="ml-auto flex items-center gap-0.5 md:hidden shrink-0">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon-xs"
|
variant="ghost"
|
||||||
className="ml-auto md:hidden shrink-0"
|
size="icon-xs"
|
||||||
onClick={() => setMobilePropsOpen(true)}
|
onClick={copyIssueToClipboard}
|
||||||
title="Properties"
|
title="Copy issue as markdown"
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="h-4 w-4" />
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
onClick={() => setMobilePropsOpen(true)}
|
||||||
|
title="Properties"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:flex items-center md:ml-auto shrink-0">
|
<div className="hidden md:flex items-center md:ml-auto shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
onClick={copyIssueToClipboard}
|
||||||
|
title="Copy issue as markdown"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
|
|||||||
Reference in New Issue
Block a user