frontend_nyla/src/contexts/FileContext.tsx

340 lines
12 KiB
TypeScript

import React, { createContext, useContext, useCallback, useState, useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import api from '../api';
import { useFileOperations, type FilePreviewResult } from '../hooks/useFiles';
import type { FolderInfo } from '../api/fileApi';
import type { FileNode } from '../components/FolderTree/FolderTree';
export type { FolderInfo };
interface FileContextType {
folders: FolderInfo[];
foldersLoading: boolean;
refreshFolders: () => Promise<void>;
treeFileNodes: FileNode[];
treeFilesLoading: boolean;
loadTreeFiles: (folderId: string) => Promise<void>;
refreshTreeFiles: () => Promise<void>;
updateTreeFileNode: (fileId: string, patch: Partial<FileNode>) => void;
expandedFolderIds: Set<string>;
toggleFolderExpanded: (id: string) => 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>;
handleMoveFolders: (folderIds: string[], targetParentId: string | null) => Promise<void>;
handleMoveFile: (fileId: string, targetFolderId: string | null) => Promise<void>;
handleMoveFiles: (fileIds: string[], targetFolderId: string | null) => Promise<void>;
handleDownloadFolder: (folderId: string, folderName: string) => Promise<void>;
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<FilePreviewResult>;
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
uploadingFile: boolean;
deletingFiles: Set<string>;
previewingFiles: Set<string>;
downloadingFiles: Set<string>;
}
export const FileContext = createContext<FileContextType | undefined>(undefined);
const _ROOT_KEY = '';
function _toFileNode(f: any): FileNode {
return {
id: f.id,
fileName: f.fileName || f.name || 'unknown',
mimeType: f.mimeType,
fileSize: f.fileSize,
folderId: f.folderId ?? null,
scope: f.scope,
neutralize: f.neutralize,
sysCreatedBy: f.sysCreatedBy,
};
}
export function FileProvider({ children }: { children: React.ReactNode }) {
const {
handleFileUpload: hookHandleFileUpload,
handleFileDelete: hookHandleFileDelete,
handleFilePreview,
handleFileDownload,
uploadingFile,
deletingFiles,
previewingFiles,
downloadingFiles,
} = useFileOperations();
// ── Derive a session-scoped storage key from the current feature-instance URL ──
const location = useLocation();
const storageKey = useMemo(() => {
const match = location.pathname.match(/^\/mandates\/([^/]+)\/([^/]+)\/([^/]+)/);
const instanceId = match ? match[3] : '_global';
return `folderTree-expandedIds-${instanceId}`;
}, [location.pathname]);
// ── Folder expanded state (persisted per feature-instance in sessionStorage) ──
const _loadExpanded = (key: string): Set<string> => {
try {
const stored = sessionStorage.getItem(key);
if (!stored) return new Set<string>();
const ids: string[] = JSON.parse(stored);
return new Set(ids.filter(id => id && id !== '__root__'));
} catch { return new Set<string>(); }
};
const [expandedFolderIds, setExpandedFolderIds] = useState<Set<string>>(() => _loadExpanded(storageKey));
useEffect(() => {
setExpandedFolderIds(_loadExpanded(storageKey));
setTreeFilesMap(new Map());
setFolders([]);
}, [storageKey]);
// ── Folder state ──────────────────────────────────────────────────────
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, storageKey]);
// ── Tree files: lazy-loaded per expanded folder ───────────────────────
const [treeFilesMap, setTreeFilesMap] = useState<Map<string, FileNode[]>>(new Map());
const [treeFilesLoading, setTreeFilesLoading] = useState(false);
const loadTreeFiles = useCallback(async (folderId: string) => {
const key = folderId || _ROOT_KEY;
setTreeFilesLoading(true);
try {
const filterValue = folderId || null;
const resp = await api.get('/api/files/list', {
params: {
pagination: JSON.stringify({
page: 1,
pageSize: 500,
filters: { folderId: filterValue },
}),
},
});
const items: any[] = resp.data?.items || [];
setTreeFilesMap(prev => {
const next = new Map(prev);
next.set(key, items.map(_toFileNode));
return next;
});
} catch (err) {
console.error(`Failed to load tree files for folder ${folderId}:`, err);
} finally {
setTreeFilesLoading(false);
}
}, []);
const _removeTreeFiles = useCallback((folderId: string) => {
const key = folderId || _ROOT_KEY;
setTreeFilesMap(prev => {
const next = new Map(prev);
next.delete(key);
return next;
});
}, []);
const refreshTreeFiles = useCallback(async () => {
const keys = Array.from(treeFilesMap.keys());
if (!keys.includes(_ROOT_KEY)) keys.push(_ROOT_KEY);
await Promise.all(
keys.map(key => loadTreeFiles(key === _ROOT_KEY ? '' : key)),
);
}, [treeFilesMap, loadTreeFiles]);
const updateTreeFileNode = useCallback((fileId: string, patch: Partial<FileNode>) => {
setTreeFilesMap(prev => {
const next = new Map<string, FileNode[]>();
let found = false;
for (const [key, files] of prev) {
const updated = files.map(f => {
if (f.id === fileId) {
found = true;
return { ...f, ...patch };
}
return f;
});
next.set(key, updated);
}
return found ? next : prev;
});
}, []);
// Load root files on mount and on context change
useEffect(() => { loadTreeFiles(''); }, [loadTreeFiles, storageKey]);
// Load files for expanded folders on mount and context change
useEffect(() => {
expandedFolderIds.forEach(id => {
if (!treeFilesMap.has(id)) {
loadTreeFiles(id);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [storageKey]);
const treeFileNodes: FileNode[] = useMemo(() => {
const result: FileNode[] = [];
for (const [, files] of treeFilesMap) {
for (const f of files) result.push(f);
}
return result;
}, [treeFilesMap]);
// ── Toggle expand: load/unload tree files ─────────────────────────────
const toggleFolderExpanded = useCallback((id: string) => {
setExpandedFolderIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
loadTreeFiles(id);
}
try { sessionStorage.setItem(storageKey, JSON.stringify([...next])); } catch {}
return next;
});
}, [storageKey, loadTreeFiles]);
// ── Folder operations ─────────────────────────────────────────────────
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 } });
_removeTreeFiles(folderId);
await refreshFolders();
}, [refreshFolders, _removeTreeFiles]);
const handleMoveFolder = useCallback(async (folderId: string, targetParentId: string | null) => {
await api.post(`/api/files/folders/${folderId}/move`, { targetParentId });
await refreshFolders();
}, [refreshFolders]);
const handleMoveFolders = useCallback(async (folderIds: string[], targetParentId: string | null) => {
await api.post('/api/files/batch-move', { folderIds, targetParentId });
await refreshFolders();
}, [refreshFolders]);
// ── File operations ───────────────────────────────────────────────────
const handleMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
await api.post(`/api/files/${fileId}/move`, { targetFolderId });
await refreshTreeFiles();
await refreshFolders();
}, [refreshTreeFiles, refreshFolders]);
const handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
await api.post('/api/files/batch-move', { fileIds, targetFolderId });
await refreshTreeFiles();
await refreshFolders();
}, [refreshTreeFiles, refreshFolders]);
const handleFileUpload = useCallback(async (file: File, workflowId?: string) => {
const result = await hookHandleFileUpload(file, workflowId);
if (result.success) {
await refreshTreeFiles();
await refreshFolders();
}
return result;
}, [hookHandleFileUpload, refreshTreeFiles, refreshFolders]);
const handleFileDelete = useCallback(async (fileId: string, onOptimisticDelete?: () => void) => {
const success = await hookHandleFileDelete(fileId, () => {
onOptimisticDelete?.();
});
if (success) {
await refreshTreeFiles();
await refreshFolders();
}
return success;
}, [hookHandleFileDelete, refreshTreeFiles, 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);
}
}, []);
return (
<FileContext.Provider
value={{
folders,
foldersLoading,
refreshFolders,
treeFileNodes,
treeFilesLoading,
loadTreeFiles,
refreshTreeFiles,
updateTreeFileNode,
expandedFolderIds,
toggleFolderExpanded,
handleCreateFolder,
handleRenameFolder,
handleDeleteFolder,
handleMoveFolder,
handleMoveFolders,
handleMoveFile,
handleMoveFiles,
handleDownloadFolder,
handleFileDelete,
handleFileUpload,
handleFilePreview,
handleFileDownload: async (fileId: string, fileName: string) => {
await handleFileDownload(fileId, fileName);
},
uploadingFile,
deletingFiles,
previewingFiles,
downloadingFiles,
}}
>
{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;
}