diff --git a/surfsense_web/components/documents/FolderNode.tsx b/surfsense_web/components/documents/FolderNode.tsx index 7f75f8abf..a1b437983 100644 --- a/surfsense_web/components/documents/FolderNode.tsx +++ b/surfsense_web/components/documents/FolderNode.tsx @@ -4,6 +4,7 @@ import { AlertCircle, ChevronDown, ChevronRight, + Download, Eye, EyeOff, Folder, @@ -80,6 +81,7 @@ interface FolderNodeProps { isWatched?: boolean; onRescan?: (folder: FolderDisplay) => void | Promise; onStopWatching?: (folder: FolderDisplay) => void; + onExportFolder?: (folder: FolderDisplay) => void; } function getDropZone( @@ -120,6 +122,7 @@ export const FolderNode = React.memo(function FolderNode({ isWatched, onRescan, onStopWatching, + onExportFolder, }: FolderNodeProps) { const [renameValue, setRenameValue] = useState(folder.name); const inputRef = useRef(null); @@ -408,6 +411,17 @@ export const FolderNode = React.memo(function FolderNode({ Move to... + {onExportFolder && ( + { + e.stopPropagation(); + onExportFolder(folder); + }} + > + + Export folder + + )} { e.stopPropagation(); @@ -449,6 +463,12 @@ export const FolderNode = React.memo(function FolderNode({ Move to... + {onExportFolder && ( + onExportFolder(folder)}> + + Export folder + + )} onDelete(folder)}> Delete diff --git a/surfsense_web/components/documents/FolderTreeView.tsx b/surfsense_web/components/documents/FolderTreeView.tsx index 6eb53da50..4988e87e7 100644 --- a/surfsense_web/components/documents/FolderTreeView.tsx +++ b/surfsense_web/components/documents/FolderTreeView.tsx @@ -44,6 +44,7 @@ interface FolderTreeViewProps { watchedFolderIds?: Set; onRescanFolder?: (folder: FolderDisplay) => void; onStopWatchingFolder?: (folder: FolderDisplay) => void; + onExportFolder?: (folder: FolderDisplay) => void; } function groupBy(items: T[], keyFn: (item: T) => string | number): Record { @@ -81,6 +82,7 @@ export function FolderTreeView({ watchedFolderIds, onRescanFolder, onStopWatchingFolder, + onExportFolder, }: FolderTreeViewProps) { const foldersByParent = useMemo(() => groupBy(folders, (f) => f.parentId ?? "root"), [folders]); @@ -259,6 +261,7 @@ export function FolderTreeView({ isWatched={watchedFolderIds?.has(f.id)} onRescan={onRescanFolder} onStopWatching={onStopWatchingFolder} + onExportFolder={onExportFolder} /> ); diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index 0f925af33..853aea641 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -445,6 +445,47 @@ export function DocumentsSidebar({ } }, [searchSpaceId, isExportingKB]); + const handleExportFolder = useCallback( + async (folder: FolderDisplay) => { + try { + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${folder.id}`, + { method: "GET" } + ); + if (!response.ok) { + const errorData = await response.json().catch(() => ({ detail: "Export failed" })); + throw new Error(errorData.detail || "Export failed"); + } + + const skipped = response.headers.get("X-Skipped-Documents"); + if (skipped && Number(skipped) > 0) { + toast.warning(`${skipped} document(s) were skipped (still processing)`); + } + + const blob = await response.blob(); + const safeName = + folder.name + .replace(/[^a-zA-Z0-9 _-]/g, "_") + .trim() + .slice(0, 80) || "folder"; + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${safeName}.zip`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + toast.success(`Folder "${folder.name}" exported`); + } catch (err) { + console.error("Folder export failed:", err); + toast.error(err instanceof Error ? err.message : "Export failed"); + } + }, + [searchSpaceId] + ); + const handleExportDocument = useCallback( async (doc: DocumentNodeDoc, format: string) => { const safeTitle = @@ -895,6 +936,7 @@ export function DocumentsSidebar({ watchedFolderIds={watchedFolderIds} onRescanFolder={handleRescanFolder} onStopWatchingFolder={handleStopWatching} + onExportFolder={handleExportFolder} />