feat folder download as zip

This commit is contained in:
ValueOn AG 2026-03-18 14:10:45 +01:00
parent 365b188fa2
commit c6d43340ff
4 changed files with 36 additions and 2 deletions

View file

@ -12,7 +12,7 @@
*/
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { FaFolder, FaFolderOpen, FaPlus, FaPen, FaTrash, FaChevronRight, FaGlobe, FaSyncAlt } from 'react-icons/fa';
import { FaFolder, FaFolderOpen, FaPlus, FaPen, FaTrash, FaChevronRight, FaGlobe, FaSyncAlt, FaDownload } from 'react-icons/fa';
import styles from './FolderTree.module.css';
/* ── Public types ──────────────────────────────────────────────────────── */
@ -61,6 +61,7 @@ export interface FolderTreeProps {
onDeleteFile?: (fileId: string) => Promise<void>;
onDeleteFiles?: (fileIds: string[]) => Promise<void>;
onDeleteFolders?: (folderIds: string[]) => Promise<void>;
onDownloadFolder?: (folderId: string, folderName: string) => Promise<void>;
}
/* ── Helpers ───────────────────────────────────────────────────────────── */
@ -285,12 +286,14 @@ interface TreeNodeProps {
onMoveFolders?: (folderIds: string[], targetParentId: string | null) => Promise<void>;
onMoveFile?: (fileId: string, targetFolderId: string | null) => Promise<void>;
onMoveFiles?: (fileIds: string[], targetFolderId: string | null) => Promise<void>;
onDownloadFolder?: (folderId: string, folderName: string) => Promise<void>;
}
function _TreeNode({
node, depth, selectedFolderId, expandedIds, showFiles, filesByFolder, sel,
onToggle, onSelect,
onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles,
onDownloadFolder,
}: TreeNodeProps) {
const [renaming, setRenaming] = useState(false);
const [renameValue, setRenameValue] = useState(node.name);
@ -436,6 +439,11 @@ function _TreeNode({
<span className={styles.folderName}>{node.name}</span>
)}
<span className={styles.actions}>
{onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title="Ordner herunterladen (ZIP)">
<FaDownload />
</button>
)}
{onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={_handleAdd} title="Neuer Unterordner">
<FaPlus />
@ -489,6 +497,7 @@ function _TreeNode({
onMoveFolders={onMoveFolders}
onMoveFile={onMoveFile}
onMoveFiles={onMoveFiles}
onDownloadFolder={onDownloadFolder}
/>
))}
{folderFiles.map((file) => (
@ -507,7 +516,7 @@ export default function FolderTree({
selectedItemIds: externalSelectedIds, onSelectionChange,
expandedIds: externalExpandedIds, onToggleExpand,
onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles,
onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh,
onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh, onDownloadFolder,
}: FolderTreeProps) {
const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(new Set());
const [rootDropOver, setRootDropOver] = useState(false);
@ -720,6 +729,7 @@ export default function FolderTree({
onMoveFolders={onMoveFolders}
onMoveFile={onMoveFile}
onMoveFiles={onMoveFiles}
onDownloadFolder={onDownloadFolder}
/>
))}
{rootFiles.map((file) => (

View file

@ -28,6 +28,7 @@ interface FileContextType {
handleMoveFile: (fileId: string, targetFolderId: string | null) => Promise<void>;
handleMoveFiles: (fileIds: string[], targetFolderId: string | null) => Promise<void>;
handleMoveFolders: (folderIds: string[], targetParentId: string | null) => Promise<void>;
handleDownloadFolder: (folderId: string, folderName: string) => Promise<void>;
expandedFolderIds: Set<string>;
toggleFolderExpanded: (id: string) => void;
}
@ -122,6 +123,24 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
await refreshFolders();
}, [refreshFolders]);
const handleDownloadFolder = useCallback(async (folderId: string, folderName: string) => {
try {
const response = await api.get(`/api/files/folders/${folderId}/download`, {
responseType: 'blob',
});
const url = window.URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${folderName}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Failed to download folder:', err);
}
}, []);
// ── File operations ────────────────────────────────────────────────────
const handleFileUpload = useCallback(async (file: File, workflowId?: string) => {
@ -174,6 +193,7 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
handleMoveFile,
handleMoveFiles,
handleMoveFolders,
handleDownloadFolder,
expandedFolderIds,
toggleFolderExpanded,
}}

View file

@ -77,6 +77,7 @@ export const FilesPage: React.FC = () => {
handleMoveFolders,
handleMoveFile,
handleMoveFiles: contextMoveFiles,
handleDownloadFolder,
expandedFolderIds,
toggleFolderExpanded,
} = useFileContext();
@ -367,6 +368,7 @@ export const FilesPage: React.FC = () => {
onDeleteFile={_handleDeleteTreeFile}
onDeleteFiles={_handleDeleteTreeFiles}
onDeleteFolders={_handleDeleteTreeFolders}
onDownloadFolder={handleDownloadFolder}
/>
</div>

View file

@ -42,6 +42,7 @@ export const FileBrowser: React.FC<FileBrowserProps> = ({
handleMoveFile,
handleMoveFiles: contextMoveFiles,
handleFileDelete,
handleDownloadFolder,
expandedFolderIds,
toggleFolderExpanded,
} = useFileContext();
@ -238,6 +239,7 @@ export const FileBrowser: React.FC<FileBrowserProps> = ({
onDeleteFile={_onDeleteFile}
onDeleteFiles={_onDeleteFiles}
onDeleteFolders={_onDeleteFolders}
onDownloadFolder={handleDownloadFolder}
/>
{_fileNodes.length === 0 && (