From af192a840553e439381998d4f9017b21fc20fd94 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 2 Jun 2026 16:10:50 +0200 Subject: [PATCH] feat(web): add download original action to editor header --- .../documents/download-original-button.tsx | 79 +++++++++++++++++++ .../components/editor-panel/editor-panel.tsx | 7 ++ 2 files changed, 86 insertions(+) create mode 100644 surfsense_web/components/documents/download-original-button.tsx diff --git a/surfsense_web/components/documents/download-original-button.tsx b/surfsense_web/components/documents/download-original-button.tsx new file mode 100644 index 000000000..b79b289b4 --- /dev/null +++ b/surfsense_web/components/documents/download-original-button.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { Download } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; +import { documentsApiService } from "@/lib/apis/documents-api.service"; +import { authenticatedFetch } from "@/lib/auth-utils"; +import { BACKEND_URL } from "@/lib/env-config"; + +interface DownloadOriginalButtonProps { + documentId: number; +} + +/** Renders only when the document has a stored ORIGINAL file; downloads it on click. */ +export function DownloadOriginalButton({ documentId }: DownloadOriginalButtonProps) { + const [originalFilename, setOriginalFilename] = useState(null); + const [downloading, setDownloading] = useState(false); + + useEffect(() => { + let active = true; + documentsApiService + .getDocumentFiles(documentId) + .then((files) => { + if (!active) return; + const original = files.find((file) => file.kind === "ORIGINAL"); + setOriginalFilename(original?.original_filename ?? null); + }) + .catch(() => { + if (active) setOriginalFilename(null); + }); + return () => { + active = false; + }; + }, [documentId]); + + if (!originalFilename) return null; + + const handleDownload = async () => { + setDownloading(true); + try { + const response = await authenticatedFetch( + `${BACKEND_URL}/api/v1/documents/${documentId}/download-original`, + { method: "GET" } + ); + if (!response.ok) throw new Error("Download failed"); + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = originalFilename; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); + toast.success("Download started"); + } catch { + toast.error("Failed to download original file"); + } finally { + setDownloading(false); + } + }; + + return ( + + ); +} diff --git a/surfsense_web/components/editor-panel/editor-panel.tsx b/surfsense_web/components/editor-panel/editor-panel.tsx index 534ff9daa..945fbc529 100644 --- a/surfsense_web/components/editor-panel/editor-panel.tsx +++ b/surfsense_web/components/editor-panel/editor-panel.tsx @@ -15,6 +15,7 @@ import dynamic from "next/dynamic"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom"; +import { DownloadOriginalButton } from "@/components/documents/download-original-button"; import { VersionHistoryButton } from "@/components/documents/version-history"; import { SourceCodeEditor } from "@/components/editor/source-code-editor"; import { @@ -584,6 +585,9 @@ export function EditorPanelContent({ documentType={editorDoc.document_type} /> )} + {!isLocalFileMode && !isMemoryMode && documentId && ( + + )}