Dosya Yöneticisi Korumalı klasör yapısı

This commit is contained in:
Sedat Öztürk 2025-10-26 12:57:58 +03:00
parent 79bd897a0b
commit 730430ac93
5 changed files with 661 additions and 165 deletions

View file

@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Collections.Generic;
namespace Kurs.Platform.FileManagement;
@ -45,4 +46,10 @@ public class SearchFilesDto
public string Query { get; set; } = string.Empty;
public string? ParentId { get; set; }
}
public class BulkDeleteDto
{
[Required]
public List<string> ItemIds { get; set; } = new();
}

View file

@ -8,6 +8,7 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Kurs.Platform.BlobStoring;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Volo.Abp;
@ -25,6 +26,14 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
private const string FileMetadataSuffix = ".metadata.json";
private const string FolderMarkerSuffix = ".folder";
private const string IndexFileName = "index.json";
// Protected system folders that cannot be deleted, renamed, or moved
private static readonly HashSet<string> ProtectedFolders = new(StringComparer.OrdinalIgnoreCase)
{
BlobContainerNames.Avatar,
BlobContainerNames.Import,
BlobContainerNames.Activity
};
public FileManagementAppService(
ICurrentTenant currentTenant,
@ -60,6 +69,36 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return id.Replace("|", "/");
}
private bool IsProtectedFolder(string path)
{
// Get the root folder name from path
var pathParts = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (pathParts.Length == 0) return false;
var rootFolder = pathParts[0];
var isProtected = ProtectedFolders.Contains(rootFolder);
Logger.LogInformation($"IsProtectedFolder - Path: '{path}', RootFolder: '{rootFolder}', IsProtected: {isProtected}");
Logger.LogInformation($"Protected folders: {string.Join(", ", ProtectedFolders)}");
return isProtected;
}
private void ValidateNotProtectedFolder(string id, string operation)
{
var decodedPath = DecodeIdAsPath(id);
Logger.LogInformation($"ValidateNotProtectedFolder - ID: {id}, DecodedPath: {decodedPath}, Operation: {operation}");
if (IsProtectedFolder(decodedPath))
{
var folderName = decodedPath.Split('/')[0];
Logger.LogWarning($"Blocked {operation} operation on protected folder: {folderName}");
throw new UserFriendlyException($"Cannot {operation} system folder '{folderName}'. This folder is protected.");
}
Logger.LogInformation($"Folder {decodedPath} is not protected, allowing {operation}");
}
private async Task<List<FileMetadata>> GetFolderIndexAsync(string? parentId = null)
{
return await GetRealCdnContentsAsync(parentId);
@ -190,19 +229,27 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
{
var items = await GetFolderIndexAsync(parentId);
var result = items.Select(metadata => new FileItemDto
{
Id = metadata.Id,
Name = metadata.Name,
Type = metadata.Type,
Size = metadata.Size,
Extension = metadata.Extension,
MimeType = metadata.MimeType,
CreatedAt = metadata.CreatedAt,
ModifiedAt = metadata.ModifiedAt,
Path = metadata.Path,
ParentId = parentId ?? string.Empty,
IsReadOnly = metadata.IsReadOnly
var result = items.Select(metadata => {
var isRootLevel = string.IsNullOrEmpty(parentId);
var isProtected = IsProtectedFolder(metadata.Path);
var finalIsReadOnly = metadata.IsReadOnly || (isRootLevel && isProtected);
Logger.LogInformation($"Item: {metadata.Name}, Path: {metadata.Path}, IsRootLevel: {isRootLevel}, IsProtected: {isProtected}, FinalIsReadOnly: {finalIsReadOnly}");
return new FileItemDto
{
Id = metadata.Id,
Name = metadata.Name,
Type = metadata.Type,
Size = metadata.Size,
Extension = metadata.Extension,
MimeType = metadata.MimeType,
CreatedAt = metadata.CreatedAt,
ModifiedAt = metadata.ModifiedAt,
Path = metadata.Path,
ParentId = parentId ?? string.Empty,
IsReadOnly = finalIsReadOnly
};
}).OrderBy(x => x.Type == "folder" ? 0 : 1).ThenBy(x => x.Name).ToList();
return new GetFilesDto { Items = result };
@ -373,6 +420,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
public async Task<FileItemDto> RenameItemAsync(string id, RenameItemDto input)
{
// Check if this is a protected system folder
ValidateNotProtectedFolder(id, "rename");
ValidateFileName(input.Name);
var metadata = await FindItemMetadataAsync(id);
@ -499,6 +549,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
public async Task DeleteItemAsync(string id)
{
// Check if this is a protected system folder
ValidateNotProtectedFolder(id, "delete");
var cdnBasePath = _configuration["App:CdnPath"];
if (string.IsNullOrEmpty(cdnBasePath))
{
@ -525,6 +578,59 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}
}
public async Task BulkDeleteItemsAsync(BulkDeleteDto input)
{
if (input.ItemIds == null || !input.ItemIds.Any())
{
throw new UserFriendlyException("No items selected for deletion");
}
var cdnBasePath = _configuration["App:CdnPath"];
if (string.IsNullOrEmpty(cdnBasePath))
{
throw new UserFriendlyException("CDN path is not configured");
}
var tenantId = _currentTenant.Id?.ToString() ?? "host";
var errors = new List<string>();
foreach (var itemId in input.ItemIds)
{
try
{
// Check if this is a protected system folder
ValidateNotProtectedFolder(itemId, "delete");
var actualPath = DecodeIdAsPath(itemId);
var fullPath = Path.Combine(cdnBasePath, tenantId, actualPath);
if (Directory.Exists(fullPath))
{
// Klasör sil (içindeki tüm dosyalar ile birlikte)
Directory.Delete(fullPath, recursive: true);
}
else if (File.Exists(fullPath))
{
// Dosya sil
File.Delete(fullPath);
}
else
{
errors.Add($"Item not found: {itemId}");
}
}
catch (Exception ex)
{
errors.Add($"Failed to delete {itemId}: {ex.Message}");
}
}
if (errors.Any())
{
throw new UserFriendlyException($"Some items could not be deleted: {string.Join(", ", errors)}");
}
}
public async Task<Stream> DownloadFileAsync(string id)
{
var cdnBasePath = _configuration["App:CdnPath"];

View file

@ -122,6 +122,15 @@ class FileManagementService {
method: 'GET',
})
}
// Bulk delete items
async bulkDeleteItems(itemIds: string[]): Promise<{ data: any }> {
return ApiService.fetchData<any>({
url: `/api/app/file-management/bulk-delete`,
method: 'POST',
data: { itemIds },
})
}
}
export default new FileManagementService()

View file

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { Helmet } from 'react-helmet'
import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui'
import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp } from 'react-icons/fa'
import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp, FaCheckSquare, FaSquare, FaTrash, FaCut, FaCopy } from 'react-icons/fa'
import Container from '@/components/shared/Container'
import { useLocalization } from '@/utils/hooks/useLocalization'
import fileManagementService from '@/services/fileManagement.service'
@ -60,7 +60,19 @@ const FileManager = () => {
setLoading(true)
const response = await fileManagementService.getItems(folderId)
// Backend returns GetFilesDto which has Items property
setItems(response.data.items || [])
const items = response.data.items || []
// Manual protection for system folders
const protectedItems = items.map(item => {
const isSystemFolder = ['avatar', 'import', 'activity'].includes(item.name.toLowerCase())
return {
...item,
isReadOnly: item.isReadOnly || isSystemFolder
}
})
console.log('Fetched items:', protectedItems)
console.log('Protected folders check:', protectedItems.filter(item => item.isReadOnly))
setItems(protectedItems)
} catch (error) {
console.error('Failed to fetch items:', error)
toast.push(<Notification type="danger">Failed to load files and folders</Notification>)
@ -227,9 +239,16 @@ const FileManager = () => {
const handleDeleteItems = async () => {
try {
setDeleting(true)
for (const item of itemsToDelete) {
await fileManagementService.deleteItem({ id: item.id })
if (itemsToDelete.length === 1) {
// Single item delete - use existing API
await fileManagementService.deleteItem({ id: itemsToDelete[0].id })
} else {
// Multiple items - use bulk delete API
const itemIds = itemsToDelete.map(item => item.id)
await fileManagementService.bulkDeleteItems(itemIds)
}
await fetchItems(currentFolderId)
setSelectedItems([])
toast.push(<Notification type="success">Items deleted successfully</Notification>)
@ -277,6 +296,194 @@ const FileManager = () => {
}
}
// 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 = () => {
setSelectedItems(filteredItems.map(item => item.id))
}
const deselectAllItems = () => {
setSelectedItems([])
}
const deleteSelectedItems = () => {
const itemsToDelete = filteredItems.filter(item => selectedItems.includes(item.id))
const deletableItems = itemsToDelete.filter(item => !item.isReadOnly)
const protectedItems = itemsToDelete.filter(item => item.isReadOnly)
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 (deletableItems.length > 0) {
openDeleteModal(deletableItems)
// Remove protected items from selection
const deletableIds = deletableItems.map(item => item.id)
setSelectedItems(prev => prev.filter(id => deletableIds.includes(id)))
}
}
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 = () => {
const clipboardData = localStorage.getItem('fileManager_clipboard')
if (clipboardData) {
try {
const clipboard = JSON.parse(clipboardData)
if (clipboard.operation === 'copy') {
toast.push(
<Notification title="Copy" type="info">
Copy functionality will be implemented soon
</Notification>
)
} else if (clipboard.operation === 'cut') {
toast.push(
<Notification title="Move" type="info">
Move functionality will be implemented soon
</Notification>
)
}
} catch (error) {
toast.push(
<Notification title="Error" type="danger">
Invalid clipboard data
</Notification>
)
}
} else {
toast.push(
<Notification title="Clipboard Empty" type="info">
No items in clipboard
</Notification>
)
}
}
return (
<Container>
<Helmet
@ -285,105 +492,222 @@ const FileManager = () => {
defaultTitle="Sözsoft Kurs Platform"
></Helmet>
{/* Toolbar */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-4 mt-2">
<div className="flex items-center gap-2">
<Button
variant="solid"
icon={<FaCloudUploadAlt />}
onClick={() => setUploadModalOpen(true)}
>
Upload Files
</Button>
<Button
variant="default"
icon={<FaFolder />}
onClick={() => setCreateFolderModalOpen(true)}
>
Create Folder
</Button>
{breadcrumbItems.length > 1 && (
<Button variant="default" icon={<FaArrowUp />} onClick={goUpOneLevel}>
Go Up
</Button>
)}
</div>
{/* Enhanced Unified Toolbar */}
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4 mt-2">
{/* Main Toolbar Row */}
<div className="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
{/* Left Section - Primary Actions */}
<div className="flex items-center gap-2 flex-wrap min-w-0">
{/* File Operations */}
<div className="flex items-center gap-2">
<Button
variant="solid"
icon={<FaCloudUploadAlt />}
onClick={() => setUploadModalOpen(true)}
size="sm"
>
Upload Files
</Button>
<Button
variant="default"
icon={<FaFolder />}
onClick={() => setCreateFolderModalOpen(true)}
size="sm"
>
Create Folder
</Button>
</div>
<div className="flex items-center gap-4">
{/* Search */}
<div className="flex items-center">
<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-64"
/>
{/* Divider */}
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600" />
{/* Clipboard Operations */}
<div className="flex items-center gap-1">
<Button
variant="plain"
icon={<FaCopy />}
onClick={copySelectedItems}
disabled={selectedItems.length === 0}
size="sm"
className="text-gray-600 hover:text-blue-600 disabled:opacity-50"
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"
title="Cut selected items"
/>
<Button
variant="plain"
onClick={pasteItems}
disabled={!hasClipboardData}
size="sm"
className="text-gray-600 hover:text-green-600 disabled:opacity-50"
title="Paste items"
>
Paste
</Button>
</div>
{/* Divider */}
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600" />
{/* Selection Actions */}
<div className="flex items-center gap-1">
{filteredItems.length > 0 && (
<Button
variant="plain"
icon={selectedItems.length === filteredItems.length ? <FaCheckSquare /> : <FaSquare />}
onClick={selectedItems.length === filteredItems.length ? deselectAllItems : selectAllItems}
size="sm"
className="text-gray-600 hover:text-blue-600"
title={selectedItems.length === filteredItems.length ? 'Deselect all items' : 'Select all items'}
>
{selectedItems.length === filteredItems.length ? 'Deselect All' : 'Select All'}
</Button>
)}
{selectedItems.length > 0 && (
<Button
variant="plain"
icon={<FaTrash />}
onClick={deleteSelectedItems}
size="sm"
className="text-gray-600 hover:text-red-600"
title="Delete selected items"
>
Delete ({selectedItems.length})
</Button>
)}
</div>
{/* Navigation */}
{breadcrumbItems.length > 1 && (
<>
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600" />
<Button
variant="plain"
icon={<FaArrowUp />}
onClick={goUpOneLevel}
size="sm"
className="text-gray-600 hover:text-blue-600"
title="Go up one level"
>
Go Up
</Button>
</>
)}
</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)',
{/* Right Section - Search, Sort, View */}
<div className="flex items-center gap-3 flex-wrap justify-end min-w-0">
{/* Search */}
<div className="flex items-center">
<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-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 }))
}
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-36"
/>
{/* View Mode */}
<div className="flex border border-gray-300 dark:border-gray-600 rounded">
<Button
variant="plain"
size="sm"
icon={<FaTh />}
className={classNames(
'rounded-r-none border-r',
viewMode === 'grid' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)}
onClick={() => setViewMode('grid')}
/>
<Button
variant="plain"
size="sm"
icon={<FaList />}
className={classNames(
'rounded-l-none',
viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)}
onClick={() => setViewMode('list')}
/>
{/* View Mode */}
<div className="flex border border-gray-300 dark:border-gray-600 rounded">
<Button
variant="plain"
size="sm"
icon={<FaTh />}
className={classNames(
'rounded-r-none border-r',
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',
viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)}
onClick={() => setViewMode('list')}
title="List view"
/>
</div>
</div>
</div>
{/* Selection Status Bar - Show when items are selected */}
{selectedItems.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-blue-700 dark:text-blue-300 font-medium">
{selectedItems.length} item{selectedItems.length !== 1 ? 's' : ''} selected
</span>
<span className="text-xs text-gray-500">
({filteredItems.filter(item => selectedItems.includes(item.id)).length} of {filteredItems.length})
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="plain"
size="xs"
onClick={deselectAllItems}
className="text-gray-500 hover:text-gray-700"
>
Clear Selection
</Button>
</div>
</div>
</div>
)}
</div>
{/* Breadcrumb */}
@ -433,6 +757,7 @@ const FileManager = () => {
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)

View file

@ -30,6 +30,7 @@ export interface FileItemProps {
onDelete?: (item: FileItemType) => void
onDownload?: (item: FileItemType) => void
onPreview?: (item: FileItemType) => void
onToggleSelect?: (item: FileItemType) => void
className?: string
}
@ -122,6 +123,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onDelete,
onDownload,
onPreview,
onToggleSelect,
className,
} = props
@ -135,6 +137,15 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onDoubleClick?.(item)
}
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation() // Prevent item selection when clicking checkbox
onToggleSelect?.(item)
}
const handleCheckboxClick = (e: React.MouseEvent) => {
e.stopPropagation() // Prevent item selection when clicking checkbox
}
const actionMenuItems: FileActionMenuItem[] = [
// Preview - sadece dosyalar için
...(item.type === 'file' && onPreview
@ -169,8 +180,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
},
]
: []),
// Rename - her şey için
...(onRename
// Rename - sadece read-only olmayanlar için
...(onRename && !item.isReadOnly
? [
{
key: 'rename',
@ -180,8 +191,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
},
]
: []),
// Move - her şey için
...(onMove
// Move - sadece read-only olmayanlar için
...(onMove && !item.isReadOnly
? [
{
key: 'move',
@ -191,8 +202,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
},
]
: []),
// Delete - her şey için
...(onDelete
// Delete - sadece read-only olmayanlar için
...(onDelete && !item.isReadOnly
? [
{
key: 'delete',
@ -205,11 +216,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
: []),
]
// Debug için
useEffect(() => {
console.log('Action menu items for', item.name, ':', actionMenuItems)
console.log('Props:', { onCreateFolder: !!onCreateFolder, onRename: !!onRename, onMove: !!onMove, onDelete: !!onDelete })
}, [actionMenuItems, item.name])
const dropdownList = (
<div className="py-1 min-w-36">
@ -276,6 +283,18 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
{/* Checkbox */}
<div className="col-span-1 flex items-center">
<input
type="checkbox"
checked={selected}
onChange={handleCheckboxChange}
onClick={handleCheckboxClick}
disabled={item.isReadOnly}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
/>
</div>
{/* File Icon or Preview */}
<div className="col-span-1 flex items-center">
<div className="w-8 h-8">
@ -290,10 +309,15 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
</div>
{/* File Name */}
<div className="col-span-4 flex items-center min-w-0">
<div className="col-span-3 flex items-center min-w-0 gap-2">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{item.name}
</p>
{item.isReadOnly && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300 flex-shrink-0">
Protected
</span>
)}
</div>
{/* File Type */}
@ -323,25 +347,29 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
{/* Action Menu */}
<div className="col-span-1 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity relative">
<button
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(!dropdownOpen)
}}
>
<HiEllipsisVertical className="h-4 w-4" />
</button>
{dropdownOpen && actionMenuItems.length > 0 && (
{!item.isReadOnly && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-1 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</div>
<button
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(!dropdownOpen)
}}
>
<HiEllipsisVertical className="h-4 w-4" />
</button>
{dropdownOpen && actionMenuItems.length > 0 && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-1 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</div>
</>
)}
</>
)}
</div>
@ -364,31 +392,45 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
{/* Action Menu */}
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
<button
className="p-1.5 rounded-full bg-white dark:bg-gray-700 shadow-md hover:shadow-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-all duration-200"
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(!dropdownOpen)
}}
>
<HiEllipsisVertical className="h-4 w-4 text-gray-600 dark:text-gray-300" />
</button>
{dropdownOpen && actionMenuItems.length > 0 && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-2 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</div>
</>
)}
{/* Checkbox */}
<div className="absolute top-3 left-3 z-10">
<input
type="checkbox"
checked={selected}
onChange={handleCheckboxChange}
onClick={handleCheckboxClick}
disabled={item.isReadOnly}
className="w-4 h-4 text-blue-600 bg-white border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
/>
</div>
{/* Action Menu */}
{!item.isReadOnly && (
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
<button
className="p-1.5 rounded-full bg-white dark:bg-gray-700 shadow-md hover:shadow-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-all duration-200"
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(!dropdownOpen)
}}
>
<HiEllipsisVertical className="h-4 w-4 text-gray-600 dark:text-gray-300" />
</button>
{dropdownOpen && actionMenuItems.length > 0 && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-2 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</div>
</>
)}
</div>
)}
{/* File/Folder Icon or Preview */}
<div className="flex justify-center mb-3">
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
@ -404,9 +446,16 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
{/* File/Folder Name and Details */}
<div className="text-center">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate mb-1">
{item.name}
</p>
<div className="flex items-center justify-center gap-1 mb-1">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{item.name}
</p>
{item.isReadOnly && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">
Protected
</span>
)}
</div>
{/* File Size and Type */}
{item.type === 'file' && (