frontend_nyla/src/contexts/FileContext.tsx
2026-03-18 14:10:45 +01:00

212 lines
8 KiB
TypeScript

import React, { createContext, useContext, useCallback, useState, useEffect } from 'react';
import api from '../api';
import { useUserFiles, useFileOperations, UserFile } from '../hooks/useFiles';
import type { FolderInfo } from '../api/fileApi';
export type { FolderInfo };
interface FileContextType {
files: UserFile[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<{ success: boolean; previewUrl?: string | null; blob?: Blob | null; isJsonContent?: boolean; decodedContent?: any; isTextContent?: boolean; error?: string }>;
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
uploadingFile: boolean;
deletingFiles: Set<string>;
previewingFiles: Set<string>;
downloadingFiles: Set<string>;
folders: FolderInfo[];
foldersLoading: boolean;
refreshFolders: () => Promise<void>;
handleCreateFolder: (name: string, parentId: string | null) => Promise<void>;
handleRenameFolder: (folderId: string, newName: string) => Promise<void>;
handleDeleteFolder: (folderId: string) => Promise<void>;
handleMoveFolder: (folderId: string, targetParentId: string | null) => Promise<void>;
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;
}
export const FileContext = createContext<FileContextType | undefined>(undefined);
export function FileProvider({ children }: { children: React.ReactNode }) {
const { data: files, loading, error, refetch: refetchFiles, removeFileOptimistically } = useUserFiles();
const {
handleFileUpload: hookHandleFileUpload,
handleFileDelete: hookHandleFileDelete,
handleFilePreview,
handleFileDownload,
uploadingFile,
deletingFiles,
previewingFiles,
downloadingFiles
} = useFileOperations();
useEffect(() => { refetchFiles(); }, []);
// ── Folder expanded state (persisted in localStorage) ───────────────────
const _STORAGE_KEY = 'folderTree-expandedIds';
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => {
try {
const stored = localStorage.getItem(_STORAGE_KEY);
return stored ? new Set<string>(JSON.parse(stored)) : new Set<string>();
} catch { return new Set<string>(); }
});
const toggleFolderExpanded = useCallback((id: string) => {
setExpandedFolderIds(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
try { localStorage.setItem(_STORAGE_KEY, JSON.stringify([...next])); } catch {}
return next;
});
}, []);
// ── Folder state (single source of truth) ──────────────────────────────
const [folders, setFolders] = useState<FolderInfo[]>([]);
const [foldersLoading, setFoldersLoading] = useState(false);
const refreshFolders = useCallback(async () => {
setFoldersLoading(true);
try {
const response = await api.get('/api/files/folders');
const data = Array.isArray(response.data) ? response.data : [];
setFolders(data);
} catch (err) {
console.error('Failed to load folders:', err);
} finally {
setFoldersLoading(false);
}
}, []);
useEffect(() => { refreshFolders(); }, [refreshFolders]);
const handleCreateFolder = useCallback(async (name: string, parentId: string | null) => {
await api.post('/api/files/folders', { name, parentId: parentId || null });
await refreshFolders();
}, [refreshFolders]);
const handleRenameFolder = useCallback(async (folderId: string, newName: string) => {
await api.put(`/api/files/folders/${folderId}`, { name: newName });
await refreshFolders();
}, [refreshFolders]);
const handleDeleteFolder = useCallback(async (folderId: string) => {
await api.delete(`/api/files/folders/${folderId}`, { params: { recursive: true } });
await refreshFolders();
await refetchFiles();
}, [refreshFolders, refetchFiles]);
const handleMoveFolder = useCallback(async (folderId: string, targetParentId: string | null) => {
await api.post(`/api/files/folders/${folderId}/move`, { targetParentId });
await refreshFolders();
}, [refreshFolders]);
const handleMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
await api.post(`/api/files/${fileId}/move`, { targetFolderId });
await refetchFiles();
}, [refetchFiles]);
const handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
await api.post('/api/files/batch-move', { fileIds, targetFolderId });
await refetchFiles();
}, [refetchFiles]);
const handleMoveFolders = useCallback(async (folderIds: string[], targetParentId: string | null) => {
await api.post('/api/files/batch-move', { folderIds, targetParentId });
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) => {
const result = await hookHandleFileUpload(file, workflowId);
if (result.success && result.fileData) {
await refetchFiles();
}
return result;
}, [hookHandleFileUpload, refetchFiles]);
const handleFileDelete = useCallback(async (fileId: string, onOptimisticDelete?: () => void) => {
const success = await hookHandleFileDelete(fileId, () => {
removeFileOptimistically(fileId);
onOptimisticDelete?.();
});
if (success) {
await refetchFiles();
}
return success;
}, [hookHandleFileDelete, removeFileOptimistically, refetchFiles]);
const refetch = useCallback(async () => {
await refetchFiles();
}, [refetchFiles]);
return (
<FileContext.Provider
value={{
files,
loading,
error,
refetch,
handleFileUpload,
handleFileDelete,
handleFilePreview: handleFilePreview as FileContextType['handleFilePreview'],
handleFileDownload: async (fileId: string, fileName: string) => {
await handleFileDownload(fileId, fileName);
},
uploadingFile,
deletingFiles,
previewingFiles,
downloadingFiles,
folders,
foldersLoading,
refreshFolders,
handleCreateFolder,
handleRenameFolder,
handleDeleteFolder,
handleMoveFolder,
handleMoveFile,
handleMoveFiles,
handleMoveFolders,
handleDownloadFolder,
expandedFolderIds,
toggleFolderExpanded,
}}
>
{children}
</FileContext.Provider>
);
}
export function useFileContext() {
const context = useContext(FileContext);
if (context === undefined) {
throw new Error('useFileContext must be used within a FileProvider');
}
return context;
}