import { useState, useEffect, useCallback, useRef } from 'react' import { Helmet } from 'react-helmet' import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui' import { useStoreState } from '@/store' import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp, FaCheckSquare, FaSquare, FaTrash, FaCut, FaCopy, FaEdit, FaDownload, FaPaste, FaBuilding, } from 'react-icons/fa' import Container from '@/components/shared/Container' import { useLocalization } from '@/utils/hooks/useLocalization' import fileManagementService from '@/services/fileManagement.service' import { getTenants } from '@/services/tenant.service' import type { TenantDto } from '@/proxy/config/models' import Breadcrumb from './components/Breadcrumb' import FileItem from './components/FileItem' import FileUploadModal from './components/FileUploadModal' import { CreateFolderModal, RenameItemModal, DeleteConfirmModal } from './components/FileModals' import type { FileItem as FileItemType, BreadcrumbItem, ViewMode, SortBy, SortOrder, FileManagerFilters, } from '@/types/fileManagement' import classNames from 'classnames' import { APP_NAME } from '@/constants/app.constant' // Select options for sorting const FileManager = () => { const { translate } = useLocalization() const authTenantId = useStoreState((state) => state.auth.tenant?.tenantId) const authTenantName = useStoreState((state) => state.auth.tenant?.tenantName) const isHostContext = !authTenantId // State const [loading, setLoading] = useState(true) const [items, setItems] = useState([]) const [filteredItems, setFilteredItems] = useState([]) const [currentFolderId, setCurrentFolderId] = useState() const [breadcrumbItems, setBreadcrumbItems] = useState([ { name: 'Files', path: '', id: undefined }, ]) const [selectedItems, setSelectedItems] = useState([]) const [viewMode, setViewMode] = useState('grid') const [filters, setFilters] = useState({ searchTerm: '', sortBy: 'name', sortOrder: 'asc', }) // Modal states const [uploadModalOpen, setUploadModalOpen] = useState(false) const [createFolderModalOpen, setCreateFolderModalOpen] = useState(false) const [renameModalOpen, setRenameModalOpen] = useState(false) const [deleteModalOpen, setDeleteModalOpen] = useState(false) const [itemToRename, setItemToRename] = useState() const [itemsToDelete, setItemsToDelete] = useState([]) // Tenant state const [tenants, setTenants] = useState([]) const [tenantsLoading, setTenantsLoading] = useState(false) const [selectedTenant, setSelectedTenant] = useState<{ id: string; name: string } | undefined>( authTenantId ? { id: authTenantId, name: authTenantName || '' } : undefined, ) // Tracks mid-flight tenant change so the fetch effect doesn't fire with a stale folderId const pendingTenantChange = useRef(false) // Loading states const [uploading, setUploading] = useState(false) const [creating, setCreating] = useState(false) const [renaming, setRenaming] = useState(false) const [deleting, setDeleting] = useState(false) // Fetch tenants const fetchTenants = useCallback(async () => { try { setTenantsLoading(true) const response = await getTenants(0, 1000) setTenants(response.data.items || []) } catch (error) { console.error('Failed to fetch tenants:', error) } finally { setTenantsLoading(false) } }, []) useEffect(() => { if (isHostContext) { fetchTenants() } }, [fetchTenants, isHostContext]) // If user is in a tenant context, lock selection to that tenant. useEffect(() => { if (!authTenantId) return setSelectedTenant((prev) => { if (prev?.id === authTenantId) return prev return { id: authTenantId, name: authTenantName || prev?.name || '' } }) }, [authTenantId, authTenantName]) // Reset navigation when tenant changes useEffect(() => { pendingTenantChange.current = true setCurrentFolderId(undefined) setSelectedItems([]) setBreadcrumbItems([{ name: 'Files', path: '', id: undefined }]) }, [selectedTenant]) // Fetch items from API const fetchItems = useCallback( async (folderId?: string) => { try { setLoading(true) const response = await fileManagementService.getItems(folderId, selectedTenant?.id) // Backend returns GetFilesDto which has Items property const items = response.data.items || [] // Manual protection for system folders const protectedItems = items.map((item) => { const isSystemFolder = ['avatar', 'import', 'note'].includes(item.name.toLowerCase()) return { ...item, isReadOnly: item.isReadOnly || isSystemFolder, } }) setItems(protectedItems) } catch (error) { console.error('Failed to fetch items:', error) toast.push(Failed to load files and folders) } finally { setLoading(false) } }, [selectedTenant], ) // Fetch breadcrumb path const fetchBreadcrumb = useCallback( async (folderId?: string) => { try { if (!folderId) { setBreadcrumbItems([{ name: 'Files', path: '', id: undefined }]) return } const response = await fileManagementService.getFolderPath(folderId, selectedTenant?.id) // console.log('Breadcrumb response for folderId:', folderId, response) const pathItems: BreadcrumbItem[] = [ { name: 'Files', path: '', id: undefined }, ...response.data.path.map((item) => ({ name: item.name, path: item.id, id: item.id, })), ] setBreadcrumbItems(pathItems) } catch (error) { console.error('Failed to fetch breadcrumb:', error) } }, [selectedTenant], ) // Initial load useEffect(() => { if (pendingTenantChange.current) { pendingTenantChange.current = false if (currentFolderId !== undefined) { // The reset effect already called setCurrentFolderId(undefined); // wait for that state update to re-trigger this effect at root. return } } fetchItems(currentFolderId) fetchBreadcrumb(currentFolderId) }, [currentFolderId, fetchItems, fetchBreadcrumb]) // Filter and sort items useEffect(() => { let filtered = [...items] // Apply search filter if (filters.searchTerm) { filtered = filtered.filter((item) => item.name.toLowerCase().includes(filters.searchTerm!.toLowerCase()), ) } // Apply sorting filtered.sort((a, b) => { let comparison = 0 switch (filters.sortBy) { case 'name': comparison = a.name.localeCompare(b.name) break case 'size': comparison = (a.size || 0) - (b.size || 0) break case 'type': comparison = a.type.localeCompare(b.type) break case 'modified': comparison = new Date(a.modifiedAt).getTime() - new Date(b.modifiedAt).getTime() break } return filters.sortOrder === 'desc' ? -comparison : comparison }) // Folders first filtered.sort((a, b) => { if (a.type === 'folder' && b.type === 'file') return -1 if (a.type === 'file' && b.type === 'folder') return 1 return 0 }) setFilteredItems(filtered) }, [items, filters]) // Navigation handlers const handleBreadcrumbNavigate = (breadcrumb: BreadcrumbItem) => { setCurrentFolderId(breadcrumb.id) setSelectedItems([]) } const handleItemSelect = (item: FileItemType) => { // Protected öğeler seçilemez if (item.isReadOnly) { return } setSelectedItems((prev) => { if (prev.includes(item.id)) { return prev.filter((id) => id !== item.id) } else { return [...prev, item.id] } }) } const handleItemDoubleClick = (item: FileItemType, event?: React.MouseEvent) => { // Prevent text selection and other default behaviors if (event) { event.preventDefault() event.stopPropagation() } // Clear any text selection that might have occurred if (window.getSelection) { const selection = window.getSelection() if (selection) { selection.removeAllRanges() } } if (item.type === 'folder') { setCurrentFolderId(item.id) setSelectedItems([]) } } // File operations const handleUploadFiles = async (files: File[]) => { try { setUploading(true) for (const file of files) { // NoteModal pattern'ini kullan - Files array ile FormData const formData = new FormData() formData.append('fileName', file.name) formData.append('Files', file) // NoteModal pattern - Files array if (currentFolderId) { formData.append('parentId', currentFolderId) } await fileManagementService.uploadFileDirectly(formData, selectedTenant?.id) } await fetchItems(currentFolderId) toast.push(Files uploaded successfully) } catch (error) { console.error('Upload failed:', error) toast.push(Failed to upload files) throw error } finally { setUploading(false) } } const handleCreateFolder = async (name: string) => { try { setCreating(true) await fileManagementService.createFolder( { name, parentId: currentFolderId, }, selectedTenant?.id, ) await fetchItems(currentFolderId) toast.push(Folder created successfully) } catch (error) { console.error('Create folder failed:', error) toast.push(Failed to create folder) throw error } finally { setCreating(false) } } const handleRenameItem = async (newName: string) => { if (!itemToRename) return try { setRenaming(true) await fileManagementService.renameItem( { id: itemToRename.id, newName, }, selectedTenant?.id, ) await fetchItems(currentFolderId) toast.push(Item renamed successfully) } catch (error) { console.error('Rename failed:', error) toast.push(Failed to rename item) throw error } finally { setRenaming(false) } } const handleDeleteItems = async () => { try { setDeleting(true) if (itemsToDelete.length === 1) { // Single item delete - use existing API await fileManagementService.deleteItem({ id: itemsToDelete[0].id }, selectedTenant?.id) } else { // Multiple items - use bulk delete API const itemIds = itemsToDelete.map((item) => item.id) await fileManagementService.bulkDeleteItems(itemIds, selectedTenant?.id) } await fetchItems(currentFolderId) setSelectedItems([]) toast.push(Items deleted successfully) } catch (error) { console.error('Delete failed:', error) toast.push(Failed to delete items) throw error } finally { setDeleting(false) } } const handleDownload = async (item: FileItemType) => { try { const blob = await fileManagementService.downloadFile(item.id, selectedTenant?.id) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = item.name document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } catch (error) { console.error('Download failed:', error) toast.push(Failed to download file) } } // Action handlers const openRenameModal = (item: FileItemType) => { setItemToRename(item) setRenameModalOpen(true) } const openDeleteModal = (items: FileItemType[]) => { setItemsToDelete(items) setDeleteModalOpen(true) } const handleSingleItemDelete = async (item: FileItemType) => { // Check if it's a protected item if (item.isReadOnly) { toast.push( Protected system folders cannot be deleted. , ) return } // Check if it's a folder containing files if (item.type === 'folder') { const hasFiles = await checkFolderHasFiles(item.id) if (hasFiles) { toast.push( Folder '{item.name}' contains files and cannot be deleted for security reasons. , ) return } } // If all checks pass, open delete modal openDeleteModal([item]) } const goUpOneLevel = () => { if (breadcrumbItems.length > 1) { const parentBreadcrumb = breadcrumbItems[breadcrumbItems.length - 2] handleBreadcrumbNavigate(parentBreadcrumb) } } // Clipboard state for paste button const [hasClipboardData, setHasClipboardData] = useState(false) useEffect(() => { const checkClipboard = () => { setHasClipboardData(!!localStorage.getItem('fileManager_clipboard')) } // Check initially checkClipboard() // Check periodically (in case another tab changes it) const interval = setInterval(checkClipboard, 1000) return () => clearInterval(interval) }, []) // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'a': e.preventDefault() selectAllItems() break case 'c': e.preventDefault() if (selectedItems.length > 0) { copySelectedItems() setHasClipboardData(true) } break case 'x': e.preventDefault() if (selectedItems.length > 0) { cutSelectedItems() setHasClipboardData(true) } break case 'v': e.preventDefault() pasteItems() break case 'Delete': case 'Backspace': e.preventDefault() if (selectedItems.length > 0) { deleteSelectedItems() } break } } else if (e.key === 'Delete') { e.preventDefault() if (selectedItems.length > 0) { deleteSelectedItems() } } } document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('keydown', handleKeyDown) } }, [selectedItems, filteredItems]) // Bulk operations const selectAllItems = () => { // Sadece protected olmayan öğeleri seç const selectableItems = filteredItems.filter((item) => !item.isReadOnly) setSelectedItems(selectableItems.map((item) => item.id)) } const deselectAllItems = () => { setSelectedItems([]) } // Check if a folder contains files (for security purposes) const checkFolderHasFiles = async (folderId: string): Promise => { try { const response = await fileManagementService.getItems(folderId, selectedTenant?.id) const items = response.data.items || [] // Check if folder contains any files (not just other folders) return items.some((item) => item.type === 'file') } catch (error) { console.error('Error checking folder contents:', error) return false } } const deleteSelectedItems = async () => { const itemsToDelete = filteredItems.filter((item) => selectedItems.includes(item.id)) const deletableItems = itemsToDelete.filter((item) => !item.isReadOnly) const protectedItems = itemsToDelete.filter((item) => item.isReadOnly) // Check for folders containing files const foldersWithFiles: string[] = [] for (const item of deletableItems) { if (item.type === 'folder') { const hasFiles = await checkFolderHasFiles(item.id) if (hasFiles) { foldersWithFiles.push(item.name) } } } // Filter out folders that contain files const finalDeletableItems = deletableItems.filter((item) => { if (item.type === 'folder') { return !foldersWithFiles.includes(item.name) } return true }) // Show warnings if (protectedItems.length > 0) { toast.push( {protectedItems.length} protected system folder(s) cannot be deleted:{' '} {protectedItems.map((i) => i.name).join(', ')} , ) } if (foldersWithFiles.length > 0) { toast.push( {foldersWithFiles.length} folder(s) containing files cannot be deleted for security reasons: {foldersWithFiles.join(', ')} , ) } if (finalDeletableItems.length > 0) { openDeleteModal(finalDeletableItems) // Remove protected items and folders with files from selection const deletableIds = finalDeletableItems.map((item) => item.id) setSelectedItems((prev) => prev.filter((id) => deletableIds.includes(id))) } else if (itemsToDelete.length > 0) { // If no items can be deleted, show info message toast.push( No items can be deleted. Selected items are either protected or folders containing files. , ) } } const copySelectedItems = () => { const itemsToCopy = filteredItems.filter((item) => selectedItems.includes(item.id)) const copyableItems = itemsToCopy.filter((item) => !item.isReadOnly) const protectedItems = itemsToCopy.filter((item) => item.isReadOnly) if (protectedItems.length > 0) { toast.push( {protectedItems.length} protected system folder(s) cannot be copied:{' '} {protectedItems.map((i) => i.name).join(', ')} , ) } if (copyableItems.length > 0) { // Store in local storage or context for paste operation localStorage.setItem( 'fileManager_clipboard', JSON.stringify({ operation: 'copy', items: copyableItems, sourceFolder: currentFolderId, }), ) setHasClipboardData(true) toast.push( {copyableItems.length} item(s) copied to clipboard , ) } } const cutSelectedItems = () => { const itemsToCut = filteredItems.filter((item) => selectedItems.includes(item.id)) const cuttableItems = itemsToCut.filter((item) => !item.isReadOnly) const protectedItems = itemsToCut.filter((item) => item.isReadOnly) if (protectedItems.length > 0) { toast.push( {protectedItems.length} protected system folder(s) cannot be moved:{' '} {protectedItems.map((i) => i.name).join(', ')} , ) } if (cuttableItems.length > 0) { // Store in local storage or context for paste operation localStorage.setItem( 'fileManager_clipboard', JSON.stringify({ operation: 'cut', items: cuttableItems, sourceFolder: currentFolderId, }), ) setHasClipboardData(true) toast.push( {cuttableItems.length} item(s) cut to clipboard , ) } } const pasteItems = async () => { const clipboardData = localStorage.getItem('fileManager_clipboard') if (!clipboardData) { toast.push( No items in clipboard , ) return } try { const clipboard = JSON.parse(clipboardData) const itemIds = clipboard.items.map((item: FileItemType) => item.id) if (clipboard.operation === 'copy') { setLoading(true) try { await fileManagementService.copyItems(itemIds, currentFolderId, selectedTenant?.id) await fetchItems(currentFolderId) toast.push( {itemIds.length} item(s) copied successfully , ) } catch (error) { console.error('Copy failed:', error) toast.push( Failed to copy items , ) } finally { setLoading(false) } } else if (clipboard.operation === 'cut') { // Aynı klasörde move yapmaya çalışırsa engelleyelim if (clipboard.sourceFolder === currentFolderId) { toast.push( Cannot move items to the same folder , ) return } setLoading(true) try { await fileManagementService.moveItems(itemIds, currentFolderId, selectedTenant?.id) await fetchItems(currentFolderId) // Clipboard'ı temizle localStorage.removeItem('fileManager_clipboard') setHasClipboardData(false) toast.push( {itemIds.length} item(s) moved successfully , ) } catch (error) { console.error('Move failed:', error) toast.push( Failed to move items , ) } finally { setLoading(false) } } } catch (error) { toast.push( Invalid clipboard data , ) } } return ( {/* Enhanced Unified Toolbar */}
{/* Main Toolbar Row */}
{/* Left Section - Primary Actions */}
{/* Tenant Selector Row */}
{isHostContext ? ( setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))} prefix={} className="w-full sm:w-36 md:w-48" />
{/* Sort */}