212 lines
8 KiB
TypeScript
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;
|
|
}
|