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.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Collections.Generic;
namespace Kurs.Platform.FileManagement; namespace Kurs.Platform.FileManagement;
@ -46,3 +47,9 @@ public class SearchFilesDto
public string? ParentId { get; set; } 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.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kurs.Platform.BlobStoring; using Kurs.Platform.BlobStoring;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Volo.Abp; using Volo.Abp;
@ -26,6 +27,14 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
private const string FolderMarkerSuffix = ".folder"; private const string FolderMarkerSuffix = ".folder";
private const string IndexFileName = "index.json"; 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( public FileManagementAppService(
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
BlobManager blobContainer, BlobManager blobContainer,
@ -60,6 +69,36 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return id.Replace("|", "/"); 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) private async Task<List<FileMetadata>> GetFolderIndexAsync(string? parentId = null)
{ {
return await GetRealCdnContentsAsync(parentId); return await GetRealCdnContentsAsync(parentId);
@ -190,7 +229,14 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
{ {
var items = await GetFolderIndexAsync(parentId); var items = await GetFolderIndexAsync(parentId);
var result = items.Select(metadata => new FileItemDto 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, Id = metadata.Id,
Name = metadata.Name, Name = metadata.Name,
@ -202,7 +248,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
ModifiedAt = metadata.ModifiedAt, ModifiedAt = metadata.ModifiedAt,
Path = metadata.Path, Path = metadata.Path,
ParentId = parentId ?? string.Empty, ParentId = parentId ?? string.Empty,
IsReadOnly = metadata.IsReadOnly IsReadOnly = finalIsReadOnly
};
}).OrderBy(x => x.Type == "folder" ? 0 : 1).ThenBy(x => x.Name).ToList(); }).OrderBy(x => x.Type == "folder" ? 0 : 1).ThenBy(x => x.Name).ToList();
return new GetFilesDto { Items = result }; return new GetFilesDto { Items = result };
@ -373,6 +420,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
public async Task<FileItemDto> RenameItemAsync(string id, RenameItemDto input) public async Task<FileItemDto> RenameItemAsync(string id, RenameItemDto input)
{ {
// Check if this is a protected system folder
ValidateNotProtectedFolder(id, "rename");
ValidateFileName(input.Name); ValidateFileName(input.Name);
var metadata = await FindItemMetadataAsync(id); var metadata = await FindItemMetadataAsync(id);
@ -499,6 +549,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
public async Task DeleteItemAsync(string id) public async Task DeleteItemAsync(string id)
{ {
// Check if this is a protected system folder
ValidateNotProtectedFolder(id, "delete");
var cdnBasePath = _configuration["App:CdnPath"]; var cdnBasePath = _configuration["App:CdnPath"];
if (string.IsNullOrEmpty(cdnBasePath)) 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) public async Task<Stream> DownloadFileAsync(string id)
{ {
var cdnBasePath = _configuration["App:CdnPath"]; var cdnBasePath = _configuration["App:CdnPath"];

View file

@ -122,6 +122,15 @@ class FileManagementService {
method: 'GET', 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() export default new FileManagementService()

View file

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui' 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 Container from '@/components/shared/Container'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import fileManagementService from '@/services/fileManagement.service' import fileManagementService from '@/services/fileManagement.service'
@ -60,7 +60,19 @@ const FileManager = () => {
setLoading(true) setLoading(true)
const response = await fileManagementService.getItems(folderId) const response = await fileManagementService.getItems(folderId)
// Backend returns GetFilesDto which has Items property // 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) { } catch (error) {
console.error('Failed to fetch items:', error) console.error('Failed to fetch items:', error)
toast.push(<Notification type="danger">Failed to load files and folders</Notification>) toast.push(<Notification type="danger">Failed to load files and folders</Notification>)
@ -227,9 +239,16 @@ const FileManager = () => {
const handleDeleteItems = async () => { const handleDeleteItems = async () => {
try { try {
setDeleting(true) 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) await fetchItems(currentFolderId)
setSelectedItems([]) setSelectedItems([])
toast.push(<Notification type="success">Items deleted successfully</Notification>) 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 ( return (
<Container> <Container>
<Helmet <Helmet
@ -285,13 +492,19 @@ const FileManager = () => {
defaultTitle="Sözsoft Kurs Platform" defaultTitle="Sözsoft Kurs Platform"
></Helmet> ></Helmet>
{/* Toolbar */} {/* Enhanced Unified Toolbar */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-4 mt-2"> <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"> <div className="flex items-center gap-2">
<Button <Button
variant="solid" variant="solid"
icon={<FaCloudUploadAlt />} icon={<FaCloudUploadAlt />}
onClick={() => setUploadModalOpen(true)} onClick={() => setUploadModalOpen(true)}
size="sm"
> >
Upload Files Upload Files
</Button> </Button>
@ -299,17 +512,98 @@ const FileManager = () => {
variant="default" variant="default"
icon={<FaFolder />} icon={<FaFolder />}
onClick={() => setCreateFolderModalOpen(true)} onClick={() => setCreateFolderModalOpen(true)}
size="sm"
> >
Create Folder Create Folder
</Button> </Button>
{breadcrumbItems.length > 1 && ( </div>
<Button variant="default" icon={<FaArrowUp />} onClick={goUpOneLevel}>
Go Up {/* 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> </Button>
)} )}
</div> </div>
<div className="flex items-center gap-4"> {/* 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>
{/* Right Section - Search, Sort, View */}
<div className="flex items-center gap-3 flex-wrap justify-end min-w-0">
{/* Search */} {/* Search */}
<div className="flex items-center"> <div className="flex items-center">
<Input <Input
@ -318,7 +612,7 @@ const FileManager = () => {
value={filters.searchTerm} value={filters.searchTerm}
onChange={(e) => setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))} onChange={(e) => setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))}
prefix={<FaSearch className="text-gray-400" />} prefix={<FaSearch className="text-gray-400" />}
className="w-64" className="w-48"
/> />
</div> </div>
@ -358,6 +652,7 @@ const FileManager = () => {
setFilters((prev) => ({ ...prev, sortBy, sortOrder })) setFilters((prev) => ({ ...prev, sortBy, sortOrder }))
} }
}} }}
className="min-w-36"
/> />
{/* View Mode */} {/* View Mode */}
@ -371,6 +666,7 @@ const FileManager = () => {
viewMode === 'grid' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600', viewMode === 'grid' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)} )}
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
title="Grid view"
/> />
<Button <Button
variant="plain" variant="plain"
@ -381,11 +677,39 @@ const FileManager = () => {
viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600', viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)} )}
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
title="List view"
/> />
</div> </div>
</div> </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 */} {/* Breadcrumb */}
<div className="mb-6"> <div className="mb-6">
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} /> <Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
@ -433,6 +757,7 @@ const FileManager = () => {
selected={selectedItems.includes(item.id)} selected={selectedItems.includes(item.id)}
onSelect={handleItemSelect} onSelect={handleItemSelect}
onDoubleClick={handleItemDoubleClick} onDoubleClick={handleItemDoubleClick}
onToggleSelect={handleItemSelect}
onCreateFolder={(parentItem) => { onCreateFolder={(parentItem) => {
// Klasör içinde yeni klasör oluşturmak için parent klasörü set et // Klasör içinde yeni klasör oluşturmak için parent klasörü set et
setCurrentFolderId(parentItem.id) setCurrentFolderId(parentItem.id)

View file

@ -30,6 +30,7 @@ export interface FileItemProps {
onDelete?: (item: FileItemType) => void onDelete?: (item: FileItemType) => void
onDownload?: (item: FileItemType) => void onDownload?: (item: FileItemType) => void
onPreview?: (item: FileItemType) => void onPreview?: (item: FileItemType) => void
onToggleSelect?: (item: FileItemType) => void
className?: string className?: string
} }
@ -122,6 +123,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onDelete, onDelete,
onDownload, onDownload,
onPreview, onPreview,
onToggleSelect,
className, className,
} = props } = props
@ -135,6 +137,15 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onDoubleClick?.(item) 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[] = [ const actionMenuItems: FileActionMenuItem[] = [
// Preview - sadece dosyalar için // Preview - sadece dosyalar için
...(item.type === 'file' && onPreview ...(item.type === 'file' && onPreview
@ -169,8 +180,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
}, },
] ]
: []), : []),
// Rename - her şey için // Rename - sadece read-only olmayanlar için
...(onRename ...(onRename && !item.isReadOnly
? [ ? [
{ {
key: 'rename', key: 'rename',
@ -180,8 +191,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
}, },
] ]
: []), : []),
// Move - her şey için // Move - sadece read-only olmayanlar için
...(onMove ...(onMove && !item.isReadOnly
? [ ? [
{ {
key: 'move', key: 'move',
@ -191,8 +202,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
}, },
] ]
: []), : []),
// Delete - her şey için // Delete - sadece read-only olmayanlar için
...(onDelete ...(onDelete && !item.isReadOnly
? [ ? [
{ {
key: 'delete', 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 = ( const dropdownList = (
<div className="py-1 min-w-36"> <div className="py-1 min-w-36">
@ -276,6 +283,18 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onClick={handleClick} onClick={handleClick}
onDoubleClick={handleDoubleClick} 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 */} {/* File Icon or Preview */}
<div className="col-span-1 flex items-center"> <div className="col-span-1 flex items-center">
<div className="w-8 h-8"> <div className="w-8 h-8">
@ -290,10 +309,15 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
</div> </div>
{/* File Name */} {/* 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"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{item.name} {item.name}
</p> </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> </div>
{/* File Type */} {/* File Type */}
@ -323,6 +347,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
{/* Action Menu */} {/* Action Menu */}
<div className="col-span-1 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity relative"> <div className="col-span-1 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity relative">
{!item.isReadOnly && (
<>
<button <button
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600" className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={(e) => { onClick={(e) => {
@ -344,6 +370,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
</div> </div>
</> </>
)} )}
</>
)}
</div> </div>
</div> </div>
) )
@ -364,7 +392,20 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
onClick={handleClick} onClick={handleClick}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}
> >
{/* 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 */} {/* Action Menu */}
{!item.isReadOnly && (
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all duration-200"> <div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
<button <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" 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"
@ -388,6 +429,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
</> </>
)} )}
</div> </div>
)}
{/* File/Folder Icon or Preview */} {/* File/Folder Icon or Preview */}
<div className="flex justify-center mb-3"> <div className="flex justify-center mb-3">
@ -404,9 +446,16 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
{/* File/Folder Name and Details */} {/* File/Folder Name and Details */}
<div className="text-center"> <div className="text-center">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate mb-1"> <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} {item.name}
</p> </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 */} {/* File Size and Type */}
{item.type === 'file' && ( {item.type === 'file' && (