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; treeFileNodes: FileNode[]; treeFilesLoading: boolean; loadTreeFiles: (folderId: string) => Promise; refreshTreeFiles: () => Promise; updateTreeFileNode: (fileId: string, patch: Partial) => void; expandedFolderIds: Set; toggleFolderExpanded: (id: string) => void; handleCreateFolder: (name: string, parentId: string | null) => Promise; handleRenameFolder: (folderId: string, newName: string) => Promise; handleDeleteFolder: (folderId: string) => Promise; handleMoveFolder: (folderId: string, targetParentId: string | null) => Promise; handleMoveFolders: (folderIds: string[], targetParentId: string | null) => Promise; handleMoveFile: (fileId: string, targetFolderId: string | null) => Promise; handleMoveFiles: (fileIds: string[], targetFolderId: string | null) => Promise; handleDownloadFolder: (folderId: string, folderName: string) => Promise; handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise; handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>; handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise; handleFileDownload: (fileId: string, fileName: string) => Promise; uploadingFile: boolean; deletingFiles: Set; previewingFiles: Set; downloadingFiles: Set; } export const FileContext = createContext(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 => { try { const stored = sessionStorage.getItem(key); if (!stored) return new Set(); const ids: string[] = JSON.parse(stored); return new Set(ids.filter(id => id && id !== '__root__')); } catch { return new Set(); } }; const [expandedFolderIds, setExpandedFolderIds] = useState>(() => _loadExpanded(storageKey)); useEffect(() => { setExpandedFolderIds(_loadExpanded(storageKey)); setTreeFilesMap(new Map()); setFolders([]); }, [storageKey]); // ── Folder state ────────────────────────────────────────────────────── const [folders, setFolders] = useState([]); 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>(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) => { setTreeFilesMap(prev => { const next = new Map(); 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 ( { await handleFileDownload(fileId, fileName); }, uploadingFile, deletingFiles, previewingFiles, downloadingFiles, }} > {children} ); } export function useFileContext() { const context = useContext(FileContext); if (context === undefined) { throw new Error('useFileContext must be used within a FileProvider'); } return context; }