2025-10-26 00:19:18 +00:00
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
|
|
|
|
import { Helmet } from 'react-helmet'
|
|
|
|
|
|
import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui'
|
2025-10-26 16:27:19 +00:00
|
|
|
|
import {
|
|
|
|
|
|
FaFolder,
|
|
|
|
|
|
FaCloudUploadAlt,
|
|
|
|
|
|
FaSearch,
|
|
|
|
|
|
FaTh,
|
|
|
|
|
|
FaList,
|
|
|
|
|
|
FaArrowUp,
|
|
|
|
|
|
FaCheckSquare,
|
|
|
|
|
|
FaSquare,
|
|
|
|
|
|
FaTrash,
|
|
|
|
|
|
FaCut,
|
|
|
|
|
|
FaCopy,
|
|
|
|
|
|
FaEdit,
|
|
|
|
|
|
FaDownload,
|
|
|
|
|
|
FaPaste,
|
|
|
|
|
|
} from 'react-icons/fa'
|
2025-10-26 00:19:18 +00:00
|
|
|
|
import Container from '@/components/shared/Container'
|
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
import fileManagementService from '@/services/fileManagement.service'
|
|
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
|
|
|
// Select options for sorting
|
|
|
|
|
|
|
|
|
|
|
|
const FileManager = () => {
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
|
|
|
|
|
|
// 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[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
// Loading states
|
|
|
|
|
|
const [uploading, setUploading] = useState(false)
|
|
|
|
|
|
const [creating, setCreating] = useState(false)
|
|
|
|
|
|
const [renaming, setRenaming] = useState(false)
|
|
|
|
|
|
const [deleting, setDeleting] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch items from API
|
|
|
|
|
|
const fetchItems = useCallback(async (folderId?: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
const response = await fileManagementService.getItems(folderId)
|
2025-10-26 08:10:02 +00:00
|
|
|
|
// Backend returns GetFilesDto which has Items property
|
2025-10-26 09:57:58 +00:00
|
|
|
|
const items = response.data.items || []
|
|
|
|
|
|
// Manual protection for system folders
|
2025-10-26 16:27:19 +00:00
|
|
|
|
const protectedItems = items.map((item) => {
|
2025-10-26 09:57:58 +00:00
|
|
|
|
const isSystemFolder = ['avatar', 'import', 'activity'].includes(item.name.toLowerCase())
|
|
|
|
|
|
return {
|
|
|
|
|
|
...item,
|
2025-10-26 16:27:19 +00:00
|
|
|
|
isReadOnly: item.isReadOnly || isSystemFolder,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
console.log('Fetched items:', protectedItems)
|
2025-10-26 16:27:19 +00:00
|
|
|
|
console.log(
|
|
|
|
|
|
'Protected folders check:',
|
|
|
|
|
|
protectedItems.filter((item) => item.isReadOnly),
|
|
|
|
|
|
)
|
2025-10-26 09:57:58 +00:00
|
|
|
|
setItems(protectedItems)
|
2025-10-26 00:19:18 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch items:', error)
|
|
|
|
|
|
toast.push(<Notification type="danger">Failed to load files and folders</Notification>)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
2025-10-26 09:27:06 +00:00
|
|
|
|
console.log('Breadcrumb response for folderId:', folderId, response)
|
2025-10-26 00:19:18 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
// Initial load
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
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) => {
|
2025-10-26 16:27:19 +00:00
|
|
|
|
// Protected öğeler seçilemez
|
|
|
|
|
|
if (item.isReadOnly) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 00:19:18 +00:00
|
|
|
|
setSelectedItems((prev) => {
|
|
|
|
|
|
if (prev.includes(item.id)) {
|
|
|
|
|
|
return prev.filter((id) => id !== item.id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return [...prev, item.id]
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 16:27:19 +00:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 00:19:18 +00:00
|
|
|
|
if (item.type === 'folder') {
|
|
|
|
|
|
setCurrentFolderId(item.id)
|
|
|
|
|
|
setSelectedItems([])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// File operations
|
|
|
|
|
|
const handleUploadFiles = async (files: File[]) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setUploading(true)
|
|
|
|
|
|
for (const file of files) {
|
2025-10-26 16:27:19 +00:00
|
|
|
|
// ActivityModal pattern'ini kullan - Files array ile FormData
|
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
|
formData.append('fileName', file.name)
|
|
|
|
|
|
formData.append('Files', file) // ActivityModal pattern - Files array
|
|
|
|
|
|
if (currentFolderId) {
|
|
|
|
|
|
formData.append('parentId', currentFolderId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('FileManager uploading:', {
|
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
|
fileSize: file.size,
|
|
|
|
|
|
fileType: file.type,
|
2025-10-26 00:19:18 +00:00
|
|
|
|
parentId: currentFolderId,
|
|
|
|
|
|
})
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
|
|
|
|
|
await fileManagementService.uploadFileDirectly(formData)
|
2025-10-26 00:19:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
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)
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
if (itemsToDelete.length === 1) {
|
|
|
|
|
|
// Single item delete - use existing API
|
|
|
|
|
|
await fileManagementService.deleteItem({ id: itemsToDelete[0].id })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Multiple items - use bulk delete API
|
2025-10-26 16:27:19 +00:00
|
|
|
|
const itemIds = itemsToDelete.map((item) => item.id)
|
2025-10-26 09:57:58 +00:00
|
|
|
|
await fileManagementService.bulkDeleteItems(itemIds)
|
2025-10-26 00:19:18 +00:00
|
|
|
|
}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 00:19:18 +00:00
|
|
|
|
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)
|
|
|
|
|
|
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 goUpOneLevel = () => {
|
|
|
|
|
|
if (breadcrumbItems.length > 1) {
|
|
|
|
|
|
const parentBreadcrumb = breadcrumbItems[breadcrumbItems.length - 2]
|
|
|
|
|
|
handleBreadcrumbNavigate(parentBreadcrumb)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
// Clipboard state for paste button
|
|
|
|
|
|
const [hasClipboardData, setHasClipboardData] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const checkClipboard = () => {
|
|
|
|
|
|
setHasClipboardData(!!localStorage.getItem('fileManager_clipboard'))
|
|
|
|
|
|
}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
// Check initially
|
|
|
|
|
|
checkClipboard()
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
// Check periodically (in case another tab changes it)
|
|
|
|
|
|
const interval = setInterval(checkClipboard, 1000)
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
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 = () => {
|
2025-10-26 16:27:19 +00:00
|
|
|
|
// Sadece protected olmayan öğeleri seç
|
|
|
|
|
|
const selectableItems = filteredItems.filter((item) => !item.isReadOnly)
|
|
|
|
|
|
setSelectedItems(selectableItems.map((item) => item.id))
|
2025-10-26 09:57:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deselectAllItems = () => {
|
|
|
|
|
|
setSelectedItems([])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deleteSelectedItems = () => {
|
2025-10-26 16:27:19 +00:00
|
|
|
|
const itemsToDelete = filteredItems.filter((item) => selectedItems.includes(item.id))
|
|
|
|
|
|
const deletableItems = itemsToDelete.filter((item) => !item.isReadOnly)
|
|
|
|
|
|
const protectedItems = itemsToDelete.filter((item) => item.isReadOnly)
|
|
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
if (protectedItems.length > 0) {
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification title="Warning" type="warning">
|
2025-10-26 16:27:19 +00:00
|
|
|
|
{protectedItems.length} protected system folder(s) cannot be deleted:{' '}
|
|
|
|
|
|
{protectedItems.map((i) => i.name).join(', ')}
|
|
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
if (deletableItems.length > 0) {
|
|
|
|
|
|
openDeleteModal(deletableItems)
|
|
|
|
|
|
// Remove protected items from selection
|
2025-10-26 16:27:19 +00:00
|
|
|
|
const deletableIds = deletableItems.map((item) => item.id)
|
|
|
|
|
|
setSelectedItems((prev) => prev.filter((id) => deletableIds.includes(id)))
|
2025-10-26 09:57:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const copySelectedItems = () => {
|
2025-10-26 16:27:19 +00:00
|
|
|
|
const itemsToCopy = filteredItems.filter((item) => selectedItems.includes(item.id))
|
|
|
|
|
|
const copyableItems = itemsToCopy.filter((item) => !item.isReadOnly)
|
|
|
|
|
|
const protectedItems = itemsToCopy.filter((item) => item.isReadOnly)
|
|
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
if (protectedItems.length > 0) {
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification title="Warning" type="warning">
|
2025-10-26 16:27:19 +00:00
|
|
|
|
{protectedItems.length} protected system folder(s) cannot be copied:{' '}
|
|
|
|
|
|
{protectedItems.map((i) => i.name).join(', ')}
|
|
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
if (copyableItems.length > 0) {
|
|
|
|
|
|
// Store in local storage or context for paste operation
|
2025-10-26 16:27:19 +00:00
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
'fileManager_clipboard',
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
operation: 'copy',
|
|
|
|
|
|
items: copyableItems,
|
|
|
|
|
|
sourceFolder: currentFolderId,
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
2025-10-26 09:57:58 +00:00
|
|
|
|
setHasClipboardData(true)
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification title="Copied" type="success">
|
|
|
|
|
|
{copyableItems.length} item(s) copied to clipboard
|
2025-10-26 16:27:19 +00:00
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cutSelectedItems = () => {
|
2025-10-26 16:27:19 +00:00
|
|
|
|
const itemsToCut = filteredItems.filter((item) => selectedItems.includes(item.id))
|
|
|
|
|
|
const cuttableItems = itemsToCut.filter((item) => !item.isReadOnly)
|
|
|
|
|
|
const protectedItems = itemsToCut.filter((item) => item.isReadOnly)
|
|
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
if (protectedItems.length > 0) {
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification title="Warning" type="warning">
|
2025-10-26 16:27:19 +00:00
|
|
|
|
{protectedItems.length} protected system folder(s) cannot be moved:{' '}
|
|
|
|
|
|
{protectedItems.map((i) => i.name).join(', ')}
|
|
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
if (cuttableItems.length > 0) {
|
|
|
|
|
|
// Store in local storage or context for paste operation
|
2025-10-26 16:27:19 +00:00
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
'fileManager_clipboard',
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
operation: 'cut',
|
|
|
|
|
|
items: cuttableItems,
|
|
|
|
|
|
sourceFolder: currentFolderId,
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
2025-10-26 09:57:58 +00:00
|
|
|
|
setHasClipboardData(true)
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification title="Cut" type="success">
|
|
|
|
|
|
{cuttableItems.length} item(s) cut to clipboard
|
2025-10-26 16:27:19 +00:00
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 16:27:19 +00:00
|
|
|
|
const pasteItems = async () => {
|
2025-10-26 09:57:58 +00:00
|
|
|
|
const clipboardData = localStorage.getItem('fileManager_clipboard')
|
2025-10-26 16:27:19 +00:00
|
|
|
|
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)
|
|
|
|
|
|
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) {
|
2025-10-26 09:57:58 +00:00
|
|
|
|
toast.push(
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<Notification title="Warning" type="warning">
|
|
|
|
|
|
Cannot move items to the same folder
|
|
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
2025-10-26 16:27:19 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fileManagementService.moveItems(itemIds, currentFolderId)
|
|
|
|
|
|
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)
|
2025-10-26 09:57:58 +00:00
|
|
|
|
toast.push(
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<Notification title="Error" type="danger">
|
|
|
|
|
|
Failed to move items
|
|
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
2025-10-26 16:27:19 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
2025-10-26 09:57:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
} catch (error) {
|
2025-10-26 09:57:58 +00:00
|
|
|
|
toast.push(
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<Notification title="Error" type="danger">
|
|
|
|
|
|
Invalid clipboard data
|
|
|
|
|
|
</Notification>,
|
2025-10-26 09:57:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 00:19:18 +00:00
|
|
|
|
return (
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<Container className="px-3 sm:px-4 md:px-6">
|
2025-10-26 08:10:02 +00:00
|
|
|
|
<Helmet
|
|
|
|
|
|
titleTemplate="%s | Sözsoft Kurs Platform"
|
|
|
|
|
|
title={translate('::' + 'App.Files')}
|
|
|
|
|
|
defaultTitle="Sözsoft Kurs Platform"
|
|
|
|
|
|
></Helmet>
|
2025-10-26 00:19:18 +00:00
|
|
|
|
|
2025-10-26 09:57:58 +00:00
|
|
|
|
{/* Enhanced Unified Toolbar */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<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">
|
2025-10-26 09:57:58 +00:00
|
|
|
|
{/* Main Toolbar Row */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4">
|
2025-10-26 09:57:58 +00:00
|
|
|
|
{/* Left Section - Primary Actions */}
|
|
|
|
|
|
<div className="flex items-center gap-2 flex-wrap min-w-0">
|
|
|
|
|
|
{/* File Operations */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
{/* 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>
|
2025-10-26 09:57:58 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Clipboard Operations */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<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"
|
|
|
|
|
|
>
|
|
|
|
|
|
Rename
|
|
|
|
|
|
</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"
|
|
|
|
|
|
>
|
|
|
|
|
|
Download
|
|
|
|
|
|
</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 && (
|
2025-10-26 09:57:58 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="plain"
|
2025-10-26 16:27:19 +00:00
|
|
|
|
icon={
|
|
|
|
|
|
selectedItems.length ===
|
|
|
|
|
|
filteredItems.filter((item) => !item.isReadOnly).length ? (
|
|
|
|
|
|
<FaCheckSquare />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<FaSquare />
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
onClick={
|
|
|
|
|
|
selectedItems.length === filteredItems.filter((item) => !item.isReadOnly).length
|
|
|
|
|
|
? deselectAllItems
|
|
|
|
|
|
: selectAllItems
|
|
|
|
|
|
}
|
2025-10-26 09:57:58 +00:00
|
|
|
|
size="sm"
|
2025-10-26 16:27:19 +00:00
|
|
|
|
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'
|
|
|
|
|
|
}
|
2025-10-26 09:57:58 +00:00
|
|
|
|
>
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<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>
|
2025-10-26 09:57:58 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Right Section - Search, Sort, View */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<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">
|
2025-10-26 09:57:58 +00:00
|
|
|
|
{/* Search */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<div className="flex items-center w-full sm:w-auto">
|
2025-10-26 09:57:58 +00:00
|
|
|
|
<Input
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
placeholder="Search files..."
|
|
|
|
|
|
value={filters.searchTerm}
|
|
|
|
|
|
onChange={(e) => setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))}
|
|
|
|
|
|
prefix={<FaSearch className="text-gray-400" />}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
className="w-full sm:w-36 md:w-48"
|
2025-10-26 09:57:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</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 }))
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
className="min-w-32 sm:min-w-36 flex-shrink-0"
|
2025-10-26 00:19:18 +00:00
|
|
|
|
/>
|
2025-10-26 09:57:58 +00:00
|
|
|
|
|
|
|
|
|
|
{/* View Mode */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<div className="flex border border-gray-300 dark:border-gray-600 rounded flex-shrink-0">
|
2025-10-26 09:57:58 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
icon={<FaTh />}
|
|
|
|
|
|
className={classNames(
|
2025-10-26 16:27:19 +00:00
|
|
|
|
'rounded-r-none border-r px-2 sm:px-3',
|
2025-10-26 09:57:58 +00:00
|
|
|
|
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(
|
2025-10-26 16:27:19 +00:00
|
|
|
|
'rounded-l-none px-2 sm:px-3',
|
2025-10-26 09:57:58 +00:00
|
|
|
|
viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
|
|
|
|
|
|
)}
|
|
|
|
|
|
onClick={() => setViewMode('list')}
|
|
|
|
|
|
title="List view"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-10-26 00:19:18 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Breadcrumb */}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<div className="mb-4 sm:mb-6 overflow-x-auto">
|
|
|
|
|
|
<div className="min-w-max">
|
|
|
|
|
|
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
|
|
|
|
|
|
</div>
|
2025-10-26 00:19:18 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-26 08:59:02 +00:00
|
|
|
|
{/* Files Grid/List */}
|
2025-10-26 00:19:18 +00:00
|
|
|
|
{loading ? (
|
|
|
|
|
|
<div className="flex justify-center items-center py-20">
|
2025-10-26 08:59:02 +00:00
|
|
|
|
<Spinner size="lg" />
|
2025-10-26 00:19:18 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2025-10-26 08:59:02 +00:00
|
|
|
|
<>
|
|
|
|
|
|
{/* List View Header */}
|
|
|
|
|
|
{viewMode === 'list' && (
|
2025-10-26 16:27:19 +00:00
|
|
|
|
<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>
|
2025-10-26 08:59:02 +00:00
|
|
|
|
<div className="col-span-1"></div> {/* Actions column */}
|
|
|
|
|
|
</div>
|
2025-10-26 00:19:18 +00:00
|
|
|
|
)}
|
2025-10-26 08:59:02 +00:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
viewMode === 'grid'
|
2025-10-26 16:27:19 +00:00
|
|
|
|
? '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'
|
2025-10-26 08:59:02 +00:00
|
|
|
|
: 'space-y-1',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2025-10-26 16:27:19 +00:00
|
|
|
|
{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={(item) => openDeleteModal([item])}
|
|
|
|
|
|
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'ı açı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
|
2025-10-26 08:59:02 +00:00
|
|
|
|
}
|
2025-10-26 16:27:19 +00:00
|
|
|
|
/>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
2025-10-26 08:59:02 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
2025-10-26 00:19:18 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 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}
|
|
|
|
|
|
/>
|
2025-10-26 08:10:02 +00:00
|
|
|
|
</Container>
|
2025-10-26 00:19:18 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default FileManager
|