diff --git a/ui/src/components/IssueDocumentsSection.tsx b/ui/src/components/IssueDocumentsSection.tsx index 0a714c56..163a0541 100644 --- a/ui/src/components/IssueDocumentsSection.tsx +++ b/ui/src/components/IssueDocumentsSection.tsx @@ -18,7 +18,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { ChevronDown, ChevronRight, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react"; +import { Check, ChevronDown, ChevronRight, Copy, Download, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react"; type DraftState = { key: string; @@ -71,6 +71,18 @@ function isDocumentConflictError(error: unknown) { return error instanceof ApiError && error.status === 409; } +function downloadDocumentFile(key: string, body: string) { + const blob = new Blob([body], { type: "text/markdown;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `${key}.md`; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); +} + export function IssueDocumentsSection({ issue, canDeleteDocuments, @@ -92,8 +104,10 @@ export function IssueDocumentsSection({ const [documentConflict, setDocumentConflict] = useState(null); const [foldedDocumentKeys, setFoldedDocumentKeys] = useState(() => loadFoldedDocumentKeys(issue.id)); const [autosaveDocumentKey, setAutosaveDocumentKey] = useState(null); + const [copiedDocumentKey, setCopiedDocumentKey] = useState(null); const [highlightDocumentKey, setHighlightDocumentKey] = useState(null); const autosaveDebounceRef = useRef | null>(null); + const copiedDocumentTimerRef = useRef | null>(null); const hasScrolledToHashRef = useRef(false); const { state: autosaveState, @@ -338,6 +352,21 @@ export function IssueDocumentsSection({ ); }, [commitDraft, documentConflict, draft]); + const copyDocumentBody = useCallback(async (key: string, body: string) => { + try { + await navigator.clipboard.writeText(body); + setCopiedDocumentKey(key); + if (copiedDocumentTimerRef.current) { + clearTimeout(copiedDocumentTimerRef.current); + } + copiedDocumentTimerRef.current = setTimeout(() => { + setCopiedDocumentKey((current) => current === key ? null : current); + }, 1400); + } catch { + setError("Could not copy document"); + } + }, []); + const handleDraftBlur = async (event: React.FocusEvent) => { if (event.currentTarget.contains(event.relatedTarget as Node | null)) return; if (autosaveDebounceRef.current) { @@ -417,6 +446,9 @@ export function IssueDocumentsSection({ if (autosaveDebounceRef.current) { clearTimeout(autosaveDebounceRef.current); } + if (copiedDocumentTimerRef.current) { + clearTimeout(copiedDocumentTimerRef.current); + } }; }, []); @@ -600,7 +632,23 @@ export function IssueDocumentsSection({ {showTitle &&

{doc.title}

} - {canDeleteDocuments && ( +
+
{!isFolded ? (