sozsoft-platform/ui/src/views/admin/files/FileManager.tsx
2026-03-22 18:05:08 +03:00

1172 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<FileItemType[]>([])
const [filteredItems, setFilteredItems] = useState<FileItemType[]>([])
const [currentFolderId, setCurrentFolderId] = useState<string | undefined>()
const [breadcrumbItems, setBreadcrumbItems] = useState<BreadcrumbItem[]>([
{ name: 'Files', path: '', id: undefined },
])
const [selectedItems, setSelectedItems] = useState<string[]>([])
const [viewMode, setViewMode] = useState<ViewMode>('grid')
const [filters, setFilters] = useState<FileManagerFilters>({
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<FileItemType | undefined>()
const [itemsToDelete, setItemsToDelete] = useState<FileItemType[]>([])
// Tenant state
const [tenants, setTenants] = useState<TenantDto[]>([])
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(<Notification type="danger">Failed to load files and folders</Notification>)
} 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(<Notification type="success">Files uploaded successfully</Notification>)
} catch (error) {
console.error('Upload failed:', error)
toast.push(<Notification type="danger">Failed to upload files</Notification>)
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(<Notification type="success">Folder created successfully</Notification>)
} catch (error) {
console.error('Create folder failed:', error)
toast.push(<Notification type="danger">Failed to create folder</Notification>)
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(<Notification type="success">Item renamed successfully</Notification>)
} catch (error) {
console.error('Rename failed:', error)
toast.push(<Notification type="danger">Failed to rename item</Notification>)
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(<Notification type="success">Items deleted successfully</Notification>)
} catch (error) {
console.error('Delete failed:', error)
toast.push(<Notification type="danger">Failed to delete items</Notification>)
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(<Notification type="danger">Failed to download file</Notification>)
}
}
// 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(
<Notification title="Warning" type="warning">
Protected system folders cannot be deleted.
</Notification>,
)
return
}
// Check if it's a folder containing files
if (item.type === 'folder') {
const hasFiles = await checkFolderHasFiles(item.id)
if (hasFiles) {
toast.push(
<Notification title="Security Warning" type="warning">
Folder '{item.name}' contains files and cannot be deleted for security reasons.
</Notification>,
)
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<boolean> => {
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(
<Notification title="Warning" type="warning">
{protectedItems.length} protected system folder(s) cannot be deleted:{' '}
{protectedItems.map((i) => i.name).join(', ')}
</Notification>,
)
}
if (foldersWithFiles.length > 0) {
toast.push(
<Notification title="Security Warning" type="warning">
{foldersWithFiles.length} folder(s) containing files cannot be deleted for security
reasons: {foldersWithFiles.join(', ')}
</Notification>,
)
}
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(
<Notification title="Info" type="info">
No items can be deleted. Selected items are either protected or folders containing files.
</Notification>,
)
}
}
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(
<Notification title="Warning" type="warning">
{protectedItems.length} protected system folder(s) cannot be copied:{' '}
{protectedItems.map((i) => i.name).join(', ')}
</Notification>,
)
}
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(
<Notification title="Copied" type="success">
{copyableItems.length} item(s) copied to clipboard
</Notification>,
)
}
}
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(
<Notification title="Warning" type="warning">
{protectedItems.length} protected system folder(s) cannot be moved:{' '}
{protectedItems.map((i) => i.name).join(', ')}
</Notification>,
)
}
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(
<Notification title="Cut" type="success">
{cuttableItems.length} item(s) cut to clipboard
</Notification>,
)
}
}
const pasteItems = async () => {
const clipboardData = localStorage.getItem('fileManager_clipboard')
if (!clipboardData) {
toast.push(
<Notification title="Clipboard Empty" type="info">
No items in clipboard
</Notification>,
)
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(
<Notification title="Success" type="success">
{itemIds.length} item(s) copied successfully
</Notification>,
)
} catch (error) {
console.error('Copy failed:', error)
toast.push(
<Notification title="Error" type="danger">
Failed to copy items
</Notification>,
)
} finally {
setLoading(false)
}
} else if (clipboard.operation === 'cut') {
// Aynı klasörde move yapmaya çalışırsa engelleyelim
if (clipboard.sourceFolder === currentFolderId) {
toast.push(
<Notification title="Warning" type="warning">
Cannot move items to the same folder
</Notification>,
)
return
}
setLoading(true)
try {
await fileManagementService.moveItems(itemIds, currentFolderId, selectedTenant?.id)
await fetchItems(currentFolderId)
// Clipboard'ı temizle
localStorage.removeItem('fileManager_clipboard')
setHasClipboardData(false)
toast.push(
<Notification title="Success" type="success">
{itemIds.length} item(s) moved successfully
</Notification>,
)
} catch (error) {
console.error('Move failed:', error)
toast.push(
<Notification title="Error" type="danger">
Failed to move items
</Notification>,
)
} finally {
setLoading(false)
}
}
} catch (error) {
toast.push(
<Notification title="Error" type="danger">
Invalid clipboard data
</Notification>,
)
}
}
return (
<Container className="px-3">
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.Files')}
defaultTitle={APP_NAME}
></Helmet>
{/* Enhanced Unified Toolbar */}
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-3 sm:p-4 mb-4 mt-2">
{/* Main Toolbar Row */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4">
{/* Left Section - Primary Actions */}
<div className="flex items-center gap-2 flex-wrap min-w-0">
{/* Tenant Selector Row */}
<div className="flex items-center gap-2">
<FaBuilding className="text-gray-500 flex-shrink-0" />
{isHostContext ? (
<Select
size="xs"
isLoading={tenantsLoading}
options={[
{ value: '', label: 'Host' },
...tenants.map((t) => ({ value: t.id ?? '', label: t.name ?? '' })),
]}
value={{
value: selectedTenant ? selectedTenant.id : '',
label: selectedTenant ? selectedTenant.name : 'Host',
}}
onChange={(option) => {
if (option && 'value' in option) {
const val = option.value as string
if (!val) {
setSelectedTenant(undefined)
} else {
const found = tenants.find((t) => t.id === val)
if (found) setSelectedTenant({ id: found.id!, name: found.name ?? '' })
}
}
}}
/>
) : (
<div
className="text-sm font-medium text-gray-700 dark:text-gray-200 truncate max-w-[220px]"
title={authTenantName || selectedTenant?.name || ''}
>
{authTenantName || selectedTenant?.name || ''}
</div>
)}
</div>
{/* File Operations */}
{/* Navigation */}
<Button
variant="plain"
icon={<FaArrowUp />}
onClick={goUpOneLevel}
size="sm"
disabled={breadcrumbItems.length <= 1}
className="text-gray-600 hover:text-blue-600 flex-shrink-0"
title="Go up one level"
/>
<Button
variant="solid"
icon={<FaCloudUploadAlt />}
onClick={() => setUploadModalOpen(true)}
size="sm"
className="flex-shrink-0"
>
<span className="hidden sm:inline">Upload Files</span>
<span className="sm:hidden">Upload</span>
</Button>
<Button
variant="default"
icon={<FaFolder />}
onClick={() => setCreateFolderModalOpen(true)}
size="sm"
className="flex-shrink-0"
>
<span className="hidden sm:inline">Create Folder</span>
<span className="sm:hidden">Create</span>
</Button>
{/* Clipboard Operations */}
<Button
variant="plain"
icon={<FaCopy />}
onClick={copySelectedItems}
disabled={selectedItems.length === 0}
size="sm"
className="text-gray-600 hover:text-blue-600 disabled:opacity-50 flex-shrink-0"
title="Copy selected items"
/>
<Button
variant="plain"
icon={<FaCut />}
onClick={cutSelectedItems}
disabled={selectedItems.length === 0}
size="sm"
className="text-gray-600 hover:text-orange-600 disabled:opacity-50 flex-shrink-0"
title="Cut selected items"
/>
<Button
variant="plain"
icon={<FaPaste />}
onClick={pasteItems}
disabled={!hasClipboardData}
size="sm"
className="text-gray-600 hover:text-green-600 disabled:opacity-50 flex-shrink-0"
title="Paste items"
/>
<Button
variant="plain"
icon={<FaEdit />}
onClick={() => {
if (selectedItems.length === 1) {
const itemToRename = filteredItems.find((item) => item.id === selectedItems[0])
if (itemToRename && !itemToRename.isReadOnly) {
openRenameModal(itemToRename)
} else {
toast.push(
<Notification title="Warning" type="warning">
Protected system folders cannot be renamed
</Notification>,
)
}
}
}}
size="sm"
disabled={selectedItems.length !== 1}
className="text-gray-600 hover:text-blue-600 flex-shrink-0"
title="Rename selected item"
></Button>
<Button
variant="plain"
icon={<FaDownload />}
onClick={() => {
if (selectedItems.length === 1) {
const itemToDownload = filteredItems.find((item) => item.id === selectedItems[0])
if (itemToDownload && itemToDownload.type === 'file') {
handleDownload(itemToDownload)
} else {
toast.push(
<Notification title="Warning" type="warning">
Only files can be downloaded
</Notification>,
)
}
}
}}
size="sm"
disabled={
selectedItems.length !== 1 ||
(() => {
const selectedItem = filteredItems.find((item) => item.id === selectedItems[0])
return selectedItem?.type !== 'file'
})()
}
className="text-gray-600 hover:text-green-600 flex-shrink-0"
title="Download selected file"
></Button>
<Button
variant="plain"
icon={<FaTrash />}
onClick={deleteSelectedItems}
size="sm"
disabled={selectedItems.length === 0}
className="text-gray-600 hover:text-red-600 flex-shrink-0"
title="Delete selected items"
>
<span>Delete {selectedItems.length > 0 && `(${selectedItems.length})`}</span>
</Button>
{/* Selection Actions */}
{filteredItems.length > 0 && (
<Button
variant="plain"
icon={
selectedItems.length ===
filteredItems.filter((item) => !item.isReadOnly).length ? (
<FaCheckSquare />
) : (
<FaSquare />
)
}
onClick={
selectedItems.length === filteredItems.filter((item) => !item.isReadOnly).length
? deselectAllItems
: selectAllItems
}
size="sm"
className="text-gray-600 hover:text-blue-600 flex-shrink-0"
title={
selectedItems.length === filteredItems.filter((item) => !item.isReadOnly).length
? 'Deselect all selectable items'
: 'Select all selectable items'
}
>
<span className="hidden lg:inline">
{selectedItems.length === filteredItems.filter((item) => !item.isReadOnly).length
? 'Deselect All'
: 'Select All'}
</span>
<span className="lg:hidden">
{selectedItems.length === filteredItems.filter((item) => !item.isReadOnly).length
? 'Deselect'
: 'Select'}
</span>
</Button>
)}
</div>
{/* Right Section - Search, Sort, View */}
<div className="flex items-center gap-2 sm:gap-3 flex-wrap justify-start lg:justify-end min-w-0 w-full lg:w-auto">
{/* Search */}
<div className="flex items-center w-full sm:w-auto">
<Input
size="sm"
placeholder="Search files..."
value={filters.searchTerm}
onChange={(e) => setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))}
prefix={<FaSearch className="text-gray-400" />}
className="w-full sm:w-36 md:w-48"
/>
</div>
{/* Sort */}
<Select
size="sm"
options={[
{ value: 'name-asc', label: 'Name (A-Z)' },
{ value: 'name-desc', label: 'Name (Z-A)' },
{ value: 'size-asc', label: 'Size (Small to Large)' },
{ value: 'size-desc', label: 'Size (Large to Small)' },
{ value: 'modified-desc', label: 'Modified (Newest)' },
{ value: 'modified-asc', label: 'Modified (Oldest)' },
]}
value={{
value: `${filters.sortBy}-${filters.sortOrder}`,
label: (() => {
const sortOptions = {
'name-asc': 'Name (A-Z)',
'name-desc': 'Name (Z-A)',
'size-asc': 'Size (Small to Large)',
'size-desc': 'Size (Large to Small)',
'modified-desc': 'Modified (Newest)',
'modified-asc': 'Modified (Oldest)',
}
return sortOptions[
`${filters.sortBy}-${filters.sortOrder}` as keyof typeof sortOptions
]
})(),
}}
onChange={(option) => {
if (option && 'value' in option) {
const [sortBy, sortOrder] = (option.value as string).split('-') as [
SortBy,
SortOrder,
]
setFilters((prev) => ({ ...prev, sortBy, sortOrder }))
}
}}
className="min-w-32 sm:min-w-36 flex-shrink-0"
/>
{/* View Mode */}
<div className="flex border border-gray-300 dark:border-gray-600 rounded flex-shrink-0">
<Button
variant="plain"
size="sm"
icon={<FaTh />}
className={classNames(
'rounded-r-none border-r px-2 sm:px-3',
viewMode === 'grid' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)}
onClick={() => setViewMode('grid')}
title="Grid view"
/>
<Button
variant="plain"
size="sm"
icon={<FaList />}
className={classNames(
'rounded-l-none px-2 sm:px-3',
viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)}
onClick={() => setViewMode('list')}
title="List view"
/>
</div>
</div>
</div>
</div>
{/* Breadcrumb */}
<div className="mb-4 sm:mb-6 overflow-x-auto">
<div className="min-w-max">
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
</div>
</div>
{/* Files Grid/List */}
{loading ? (
<div className="flex justify-center items-center py-20">
<Spinner size="lg" />
</div>
) : (
<>
{/* List View Header */}
{viewMode === 'list' && (
<div className="hidden sm:grid grid-cols-12 gap-4 px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 border-b dark:border-gray-700 mb-2">
<div className="col-span-5 lg:col-span-5">İsim</div>
<div className="col-span-2 lg:col-span-2">Tür</div>
<div className="col-span-2 lg:col-span-2">Boyut</div>
<div className="col-span-2 lg:col-span-2">Değiştirilme</div>
<div className="col-span-1"></div> {/* Actions column */}
</div>
)}
<div
className={classNames(
viewMode === 'grid'
? 'grid grid-cols-2 xs:grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 gap-3 sm:gap-4'
: 'space-y-1',
)}
>
{filteredItems.length === 0 ? (
<div className="col-span-full text-center py-12 sm:py-20">
<FaFolder className="mx-auto h-12 w-12 sm:h-16 sm:w-16 text-gray-400 mb-4" />
<p className="text-gray-500 dark:text-gray-400 text-sm sm:text-base px-4">
{filters.searchTerm ? 'No files match your search' : 'This folder is empty'}
</p>
</div>
) : (
filteredItems.map((item) => (
<FileItem
key={item.id}
item={item}
viewMode={viewMode}
selected={selectedItems.includes(item.id)}
onSelect={handleItemSelect}
onDoubleClick={handleItemDoubleClick}
onToggleSelect={handleItemSelect}
onCreateFolder={(parentItem) => {
// Klasör içinde yeni klasör oluşturmak için parent klasörü set et
setCurrentFolderId(parentItem.id)
setCreateFolderModalOpen(true)
}}
onRename={openRenameModal}
onMove={(item) => {
// Move işlemi için öğeyi cut olarak clipboard'a koy
cutSelectedItems()
if (!selectedItems.includes(item.id)) {
setSelectedItems([item.id])
localStorage.setItem(
'fileManager_clipboard',
JSON.stringify({
operation: 'cut',
items: [item],
sourceFolder: currentFolderId,
}),
)
setHasClipboardData(true)
toast.push(
<Notification title="Cut" type="success">
Item ready to move. Navigate to target folder and paste.
</Notification>,
)
}
}}
onDelete={handleSingleItemDelete}
onDownload={item.type === 'file' ? handleDownload : undefined}
onPreview={
item.type === 'file'
? (item) => {
// Preview işlevi - resimler için modal açabiliriz
if (item.mimeType?.startsWith('image/')) {
// Resim preview modal'ıılabilir
toast.push(
<Notification type="info">
Resim önizleme özelliği yakında eklenecek
</Notification>,
)
} else {
// Diğer dosya tipleri için download
handleDownload(item)
}
}
: undefined
}
/>
))
)}
</div>
</>
)}
{/* Modals */}
<FileUploadModal
isOpen={uploadModalOpen}
onClose={() => setUploadModalOpen(false)}
onUpload={handleUploadFiles}
currentFolderId={currentFolderId}
loading={uploading}
/>
<CreateFolderModal
isOpen={createFolderModalOpen}
onClose={() => setCreateFolderModalOpen(false)}
onCreate={handleCreateFolder}
loading={creating}
currentFolderId={currentFolderId}
/>
<RenameItemModal
isOpen={renameModalOpen}
onClose={() => {
setRenameModalOpen(false)
setItemToRename(undefined)
}}
onRename={handleRenameItem}
item={itemToRename}
loading={renaming}
/>
<DeleteConfirmModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false)
setItemsToDelete([])
}}
onDelete={handleDeleteItems}
items={itemsToDelete}
loading={deleting}
/>
</Container>
)
}
export default FileManager