mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat: add folder-level export to context menu
This commit is contained in:
parent
c38239a995
commit
89f210bf7e
3 changed files with 65 additions and 0 deletions
|
|
@ -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<void>;
|
||||
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<HTMLInputElement>(null);
|
||||
|
|
@ -408,6 +411,17 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</DropdownMenuItem>
|
||||
{onExportFolder && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onExportFolder(folder);
|
||||
}}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export folder
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -449,6 +463,12 @@ export const FolderNode = React.memo(function FolderNode({
|
|||
<Move className="mr-2 h-4 w-4" />
|
||||
Move to...
|
||||
</ContextMenuItem>
|
||||
{onExportFolder && (
|
||||
<ContextMenuItem onClick={() => onExportFolder(folder)}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export folder
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem onClick={() => onDelete(folder)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ interface FolderTreeViewProps {
|
|||
watchedFolderIds?: Set<number>;
|
||||
onRescanFolder?: (folder: FolderDisplay) => void;
|
||||
onStopWatchingFolder?: (folder: FolderDisplay) => void;
|
||||
onExportFolder?: (folder: FolderDisplay) => void;
|
||||
}
|
||||
|
||||
function groupBy<T>(items: T[], keyFn: (item: T) => string | number): Record<string | number, T[]> {
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue