Dosya Yöneticisi ana klasör tools düzeltildi
This commit is contained in:
parent
c93007cc07
commit
f839d1fec0
4 changed files with 401 additions and 121 deletions
|
|
@ -8,7 +8,6 @@ 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;
|
||||
|
|
@ -200,42 +199,50 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
|||
{
|
||||
ValidateFileName(input.Name);
|
||||
|
||||
var items = await GetFolderIndexAsync(input.ParentId);
|
||||
|
||||
if (items.Any(x => x.Name.Equals(input.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
var cdnBasePath = _configuration["App:CdnPath"];
|
||||
if (string.IsNullOrEmpty(cdnBasePath))
|
||||
{
|
||||
throw new UserFriendlyException("A folder or file with this name already exists");
|
||||
throw new UserFriendlyException("CDN path is not configured");
|
||||
}
|
||||
|
||||
var folderId = GenerateFileId();
|
||||
var folderPath = string.IsNullOrEmpty(input.ParentId)
|
||||
? input.Name
|
||||
: $"{input.ParentId}/{input.Name}";
|
||||
var tenantId = _currentTenant.Id?.ToString() ?? "host";
|
||||
var parentPath = Path.Combine(cdnBasePath, tenantId);
|
||||
|
||||
if (!string.IsNullOrEmpty(input.ParentId))
|
||||
{
|
||||
parentPath = Path.Combine(parentPath, input.ParentId);
|
||||
}
|
||||
|
||||
var folderPath = Path.Combine(parentPath, input.Name);
|
||||
|
||||
// Klasör zaten var mı kontrol et
|
||||
if (Directory.Exists(folderPath))
|
||||
{
|
||||
throw new UserFriendlyException("A folder with this name already exists");
|
||||
}
|
||||
|
||||
// Dosya ile aynı isimde bir şey var mı kontrol et
|
||||
if (File.Exists(folderPath))
|
||||
{
|
||||
throw new UserFriendlyException("A file with this name already exists");
|
||||
}
|
||||
|
||||
// Klasörü oluştur
|
||||
Directory.CreateDirectory(folderPath);
|
||||
|
||||
var metadata = new FileMetadata
|
||||
{
|
||||
Id = folderId,
|
||||
Id = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}",
|
||||
Name = input.Name,
|
||||
Type = "folder",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ModifiedAt = DateTime.UtcNow,
|
||||
Path = folderPath,
|
||||
Path = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}",
|
||||
ParentId = input.ParentId ?? string.Empty,
|
||||
IsReadOnly = false,
|
||||
TenantId = _currentTenant.Id?.ToString()
|
||||
};
|
||||
|
||||
// Create folder marker blob
|
||||
var folderMarkerPath = GetTenantPrefix() + folderPath + FolderMarkerSuffix;
|
||||
await _blobContainer.SaveAsync(folderMarkerPath, Array.Empty<byte>());
|
||||
|
||||
// Create folder index
|
||||
await SaveFolderIndexAsync(new List<FileMetadata>(), folderId);
|
||||
|
||||
// Update parent index
|
||||
items.Add(metadata);
|
||||
await SaveFolderIndexAsync(items, input.ParentId);
|
||||
|
||||
return new FileItemDto
|
||||
{
|
||||
Id = metadata.Id,
|
||||
|
|
@ -267,18 +274,37 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
|||
|
||||
var blobPath = GetTenantPrefix() + filePath;
|
||||
|
||||
// Save file content
|
||||
// Save file content to CDN path
|
||||
var cdnBasePath = _configuration["App:CdnPath"];
|
||||
if (string.IsNullOrEmpty(cdnBasePath))
|
||||
{
|
||||
throw new UserFriendlyException("CDN path is not configured");
|
||||
}
|
||||
|
||||
var tenantId = _currentTenant.Id?.ToString() ?? "host";
|
||||
var fullCdnPath = Path.Combine(cdnBasePath, tenantId);
|
||||
|
||||
if (!string.IsNullOrEmpty(input.ParentId))
|
||||
{
|
||||
fullCdnPath = Path.Combine(fullCdnPath, input.ParentId);
|
||||
}
|
||||
|
||||
// Dizini oluştur
|
||||
Directory.CreateDirectory(fullCdnPath);
|
||||
|
||||
var fullFilePath = Path.Combine(fullCdnPath, input.FileName);
|
||||
|
||||
long fileSize;
|
||||
if (input.FileStream != null)
|
||||
{
|
||||
input.FileStream.Position = 0;
|
||||
await _blobContainer.SaveAsync(blobPath, input.FileStream);
|
||||
using var fileStream = File.Create(fullFilePath);
|
||||
await input.FileStream.CopyToAsync(fileStream);
|
||||
fileSize = input.FileStream.Length;
|
||||
}
|
||||
else if (input.FileContent != null)
|
||||
{
|
||||
using var stream = new MemoryStream(input.FileContent);
|
||||
await _blobContainer.SaveAsync(blobPath, stream);
|
||||
await File.WriteAllBytesAsync(fullFilePath, input.FileContent);
|
||||
fileSize = input.FileContent.Length;
|
||||
}
|
||||
else
|
||||
|
|
@ -452,42 +478,48 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
|||
|
||||
public async Task DeleteItemAsync(string id)
|
||||
{
|
||||
var metadata = await FindItemMetadataAsync(id);
|
||||
if (metadata == null)
|
||||
var cdnBasePath = _configuration["App:CdnPath"];
|
||||
if (string.IsNullOrEmpty(cdnBasePath))
|
||||
{
|
||||
throw new UserFriendlyException("Item not found");
|
||||
throw new UserFriendlyException("CDN path is not configured");
|
||||
}
|
||||
|
||||
var blobPath = GetTenantPrefix() + metadata.Path;
|
||||
var tenantId = _currentTenant.Id?.ToString() ?? "host";
|
||||
var fullPath = Path.Combine(cdnBasePath, tenantId, id);
|
||||
|
||||
if (metadata.Type == "folder")
|
||||
if (Directory.Exists(fullPath))
|
||||
{
|
||||
// Delete folder marker and index
|
||||
await _blobContainer.DeleteAsync(blobPath + FolderMarkerSuffix);
|
||||
await _blobContainer.DeleteAsync(GetTenantPrefix() + $"{id}/{IndexFileName}");
|
||||
// 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
|
||||
{
|
||||
// Delete file
|
||||
await _blobContainer.DeleteAsync(blobPath);
|
||||
throw new UserFriendlyException("Item not found");
|
||||
}
|
||||
|
||||
// Remove from parent index
|
||||
var parentItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId);
|
||||
parentItems.RemoveAll(x => x.Id == id);
|
||||
await SaveFolderIndexAsync(parentItems, metadata.ParentId == string.Empty ? null : metadata.ParentId);
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadFileAsync(string id)
|
||||
{
|
||||
var metadata = await FindItemMetadataAsync(id);
|
||||
if (metadata == null || metadata.Type != "file")
|
||||
var cdnBasePath = _configuration["App:CdnPath"];
|
||||
if (string.IsNullOrEmpty(cdnBasePath))
|
||||
{
|
||||
throw new UserFriendlyException("CDN path is not configured");
|
||||
}
|
||||
|
||||
var tenantId = _currentTenant.Id?.ToString() ?? "host";
|
||||
var fullFilePath = Path.Combine(cdnBasePath, tenantId, id);
|
||||
|
||||
if (!File.Exists(fullFilePath))
|
||||
{
|
||||
throw new UserFriendlyException("File not found");
|
||||
}
|
||||
|
||||
var blobPath = GetTenantPrefix() + metadata.Path;
|
||||
return await _blobContainer.GetAsync(blobPath);
|
||||
return File.OpenRead(fullFilePath);
|
||||
}
|
||||
|
||||
public async Task<Stream> GetFilePreviewAsync(string id)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ public class BlobManager : DomainService
|
|||
// Default container methods (for FileManagement and other general purposes)
|
||||
private IBlobContainer GetDefaultContainer()
|
||||
{
|
||||
return _blobContainerFactory.Create("");
|
||||
return _blobContainerFactory.Create("default");
|
||||
}
|
||||
|
||||
public async Task SaveAsync(string blobName, Stream bytes, bool overrideExisting = true)
|
||||
|
|
|
|||
|
|
@ -390,19 +390,32 @@ const FileManager = () => {
|
|||
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{/* Files Grid/List */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-20">
|
||||
<Spinner size={40} />
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
viewMode === 'grid'
|
||||
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4'
|
||||
: 'space-y-2',
|
||||
<>
|
||||
{/* List View Header */}
|
||||
{viewMode === 'list' && (
|
||||
<div className="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-1"></div> {/* Icon column */}
|
||||
<div className="col-span-4">İsim</div>
|
||||
<div className="col-span-2">Tür</div>
|
||||
<div className="col-span-2">Boyut</div>
|
||||
<div className="col-span-2">Değiştirilme</div>
|
||||
<div className="col-span-1"></div> {/* Actions column */}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
viewMode === 'grid'
|
||||
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4'
|
||||
: 'space-y-1',
|
||||
)}
|
||||
>
|
||||
{filteredItems.length === 0 ? (
|
||||
<div className="col-span-full text-center py-20">
|
||||
<FaFolder className="mx-auto h-16 w-16 text-gray-400 mb-4" />
|
||||
|
|
@ -415,17 +428,37 @@ const FileManager = () => {
|
|||
<FileItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
viewMode={viewMode}
|
||||
selected={selectedItems.includes(item.id)}
|
||||
onSelect={handleItemSelect}
|
||||
onDoubleClick={handleItemDoubleClick}
|
||||
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şlevi henüz implement edilmedi
|
||||
toast.push(<Notification type="info">Move özelliği yakında eklenecek</Notification>)
|
||||
}}
|
||||
onDelete={(item) => openDeleteModal([item])}
|
||||
onDownload={item.type === 'file' ? handleDownload : undefined}
|
||||
className={viewMode === 'list' ? 'flex items-center p-3 space-x-4' : 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}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { forwardRef, useState } from 'react'
|
||||
import { forwardRef, useState, useEffect } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
HiFolder,
|
||||
HiFolderPlus,
|
||||
HiDocument,
|
||||
HiPhoto,
|
||||
HiFilm,
|
||||
|
|
@ -14,14 +15,16 @@ import {
|
|||
HiArrowDownTray,
|
||||
HiEye,
|
||||
} from 'react-icons/hi2'
|
||||
import { Dropdown } from '@/components/ui'
|
||||
// import { Dropdown } from '@/components/ui' // Artık kullanmıyoruz
|
||||
import type { FileItem as FileItemType, FileActionMenuItem } from '@/types/fileManagement'
|
||||
|
||||
export interface FileItemProps {
|
||||
item: FileItemType
|
||||
selected?: boolean
|
||||
viewMode?: 'grid' | 'list'
|
||||
onSelect?: (item: FileItemType) => void
|
||||
onDoubleClick?: (item: FileItemType) => void
|
||||
onCreateFolder?: (parentItem: FileItemType) => void
|
||||
onRename?: (item: FileItemType) => void
|
||||
onMove?: (item: FileItemType) => void
|
||||
onDelete?: (item: FileItemType) => void
|
||||
|
|
@ -30,31 +33,33 @@ export interface FileItemProps {
|
|||
className?: string
|
||||
}
|
||||
|
||||
const getFileIcon = (item: FileItemType) => {
|
||||
const getFileIcon = (item: FileItemType, large: boolean = false) => {
|
||||
const iconSize = large ? "h-12 w-12" : "h-8 w-8"
|
||||
|
||||
if (item.type === 'folder') {
|
||||
return <HiFolder className="h-8 w-8 text-blue-500" />
|
||||
return <HiFolder className={`${iconSize} text-blue-500`} />
|
||||
}
|
||||
|
||||
const extension = item.extension?.toLowerCase()
|
||||
const mimeType = item.mimeType?.toLowerCase()
|
||||
|
||||
if (mimeType?.startsWith('image/')) {
|
||||
return <HiPhoto className="h-8 w-8 text-green-500" />
|
||||
return <HiPhoto className={`${iconSize} text-green-500`} />
|
||||
}
|
||||
|
||||
if (mimeType?.startsWith('video/')) {
|
||||
return <HiFilm className="h-8 w-8 text-purple-500" />
|
||||
return <HiFilm className={`${iconSize} text-purple-500`} />
|
||||
}
|
||||
|
||||
if (mimeType?.startsWith('audio/')) {
|
||||
return <HiMusicalNote className="h-8 w-8 text-pink-500" />
|
||||
return <HiMusicalNote className={`${iconSize} text-pink-500`} />
|
||||
}
|
||||
|
||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) {
|
||||
return <HiArchiveBox className="h-8 w-8 text-orange-500" />
|
||||
return <HiArchiveBox className={`${iconSize} text-orange-500`} />
|
||||
}
|
||||
|
||||
return <HiDocument className="h-8 w-8 text-gray-500" />
|
||||
return <HiDocument className={`${iconSize} text-gray-500`} />
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes?: number): string => {
|
||||
|
|
@ -67,12 +72,51 @@ const formatFileSize = (bytes?: number): string => {
|
|||
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const formatDate = (date?: string | Date): string => {
|
||||
if (!date) return ''
|
||||
|
||||
try {
|
||||
const dateObj = typeof date === 'string' ? new Date(date) : date
|
||||
return dateObj.toLocaleDateString('tr-TR', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const getFileTypeLabel = (item: FileItemType): string => {
|
||||
if (item.type === 'folder') return 'Klasör'
|
||||
|
||||
const extension = item.extension?.toLowerCase()
|
||||
const mimeType = item.mimeType?.toLowerCase()
|
||||
|
||||
if (mimeType?.startsWith('image/')) return 'Resim'
|
||||
if (mimeType?.startsWith('video/')) return 'Video'
|
||||
if (mimeType?.startsWith('audio/')) return 'Ses'
|
||||
if (mimeType?.includes('pdf')) return 'PDF'
|
||||
if (mimeType?.includes('word')) return 'Word'
|
||||
if (mimeType?.includes('excel') || mimeType?.includes('spreadsheet')) return 'Excel'
|
||||
if (mimeType?.includes('powerpoint') || mimeType?.includes('presentation')) return 'PowerPoint'
|
||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) return 'Arşiv'
|
||||
if (['txt', 'md'].includes(extension || '')) return 'Metin'
|
||||
if (['json', 'xml', 'css', 'js', 'ts', 'html'].includes(extension || '')) return 'Kod'
|
||||
|
||||
return extension?.toUpperCase() || 'Dosya'
|
||||
}
|
||||
|
||||
const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
||||
const {
|
||||
item,
|
||||
selected = false,
|
||||
viewMode = 'grid',
|
||||
onSelect,
|
||||
onDoubleClick,
|
||||
onCreateFolder,
|
||||
onRename,
|
||||
onMove,
|
||||
onDelete,
|
||||
|
|
@ -92,119 +136,290 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
|||
}
|
||||
|
||||
const actionMenuItems: FileActionMenuItem[] = [
|
||||
// Preview - sadece dosyalar için
|
||||
...(item.type === 'file' && onPreview
|
||||
? [
|
||||
{
|
||||
key: 'preview',
|
||||
label: 'Preview',
|
||||
label: 'Önizle',
|
||||
icon: 'HiEye',
|
||||
onClick: () => onPreview(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Download - sadece dosyalar için
|
||||
...(item.type === 'file' && onDownload
|
||||
? [
|
||||
{
|
||||
key: 'download',
|
||||
label: 'Download',
|
||||
label: 'İndir',
|
||||
icon: 'HiArrowDownTray',
|
||||
onClick: () => onDownload(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
icon: 'HiPencil',
|
||||
onClick: () => onRename?.(item),
|
||||
},
|
||||
{
|
||||
key: 'move',
|
||||
label: 'Move',
|
||||
icon: 'HiArrowRightOnRectangle',
|
||||
onClick: () => onMove?.(item),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
icon: 'HiTrash',
|
||||
dangerous: true,
|
||||
onClick: () => onDelete?.(item),
|
||||
},
|
||||
// Create Folder - sadece klasörler için
|
||||
...(item.type === 'folder' && onCreateFolder
|
||||
? [
|
||||
{
|
||||
key: 'createFolder',
|
||||
label: 'Yeni Klasör',
|
||||
icon: 'HiFolderPlus',
|
||||
onClick: () => onCreateFolder(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Rename - her şey için
|
||||
...(onRename
|
||||
? [
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Yeniden Adlandır',
|
||||
icon: 'HiPencil',
|
||||
onClick: () => onRename(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Move - her şey için
|
||||
...(onMove
|
||||
? [
|
||||
{
|
||||
key: 'move',
|
||||
label: 'Taşı',
|
||||
icon: 'HiArrowRightOnRectangle',
|
||||
onClick: () => onMove(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Delete - her şey için
|
||||
...(onDelete
|
||||
? [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Sil',
|
||||
icon: 'HiTrash',
|
||||
dangerous: true,
|
||||
onClick: () => onDelete(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
// 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">
|
||||
<div className="py-1 min-w-36">
|
||||
{actionMenuItems.map((menuItem) => (
|
||||
<div
|
||||
key={menuItem.key}
|
||||
className={classNames(
|
||||
'flex items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700',
|
||||
menuItem.dangerous && 'text-red-600 dark:text-red-400',
|
||||
'flex items-center px-2 py-1 text-xs cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
|
||||
menuItem.dangerous && 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20',
|
||||
)}
|
||||
onClick={() => {
|
||||
menuItem.onClick()
|
||||
setDropdownOpen(false)
|
||||
}}
|
||||
>
|
||||
{menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-2" />}
|
||||
{menuItem.icon === 'HiArrowDownTray' && <HiArrowDownTray className="h-4 w-4 mr-2" />}
|
||||
{menuItem.icon === 'HiPencil' && <HiPencil className="h-4 w-4 mr-2" />}
|
||||
{menuItem.icon === 'HiFolderPlus' && <HiFolderPlus className="h-4 w-4 mr-3 text-blue-500" />}
|
||||
{menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-3 text-gray-500" />}
|
||||
{menuItem.icon === 'HiArrowDownTray' && <HiArrowDownTray className="h-4 w-4 mr-3 text-green-500" />}
|
||||
{menuItem.icon === 'HiPencil' && <HiPencil className="h-4 w-4 mr-3 text-orange-500" />}
|
||||
{menuItem.icon === 'HiArrowRightOnRectangle' && (
|
||||
<HiArrowRightOnRectangle className="h-4 w-4 mr-2" />
|
||||
<HiArrowRightOnRectangle className="h-4 w-4 mr-3 text-purple-500" />
|
||||
)}
|
||||
{menuItem.icon === 'HiTrash' && <HiTrash className="h-4 w-4 mr-2" />}
|
||||
{menuItem.label}
|
||||
{menuItem.icon === 'HiTrash' && <HiTrash className="h-4 w-4 mr-3" />}
|
||||
<span className="flex-1">{menuItem.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Resim preview komponenti
|
||||
const ImagePreview = ({ src, alt }: { src: string; alt: string }) => {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="w-full h-full bg-gray-100 dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden">
|
||||
{!imageError ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="max-w-full max-h-full object-cover"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
{getFileIcon(item, false)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'relative group grid grid-cols-12 gap-4 p-3 border rounded-lg cursor-pointer transition-all duration-200',
|
||||
'hover:border-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700/50',
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800',
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{/* File Icon or Preview */}
|
||||
<div className="col-span-1 flex items-center">
|
||||
<div className="w-8 h-8">
|
||||
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
|
||||
<ImagePreview src={`/api/app/file-management/${item.id}/download-file`} alt={item.name} />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
{getFileIcon(item, false)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Name */}
|
||||
<div className="col-span-4 flex items-center min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{item.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* File Type */}
|
||||
<div className="col-span-2 flex items-center">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{getFileTypeLabel(item)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* File Size */}
|
||||
<div className="col-span-2 flex items-center">
|
||||
{item.type === 'file' && item.size ? (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formatFileSize(item.size)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">-</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modified Date */}
|
||||
<div className="col-span-2 flex items-center">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formatDate(item.modifiedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 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 && (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Grid view (varsayılan)
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'relative group p-4 border rounded-lg cursor-pointer transition-all duration-200',
|
||||
'hover:border-blue-300 hover:shadow-md',
|
||||
'relative group p-4 border rounded-xl cursor-pointer transition-all duration-300',
|
||||
'hover:border-blue-400 hover:shadow-lg hover:-translate-y-1',
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-md'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800',
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-lg'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{/* Action Menu */}
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Dropdown
|
||||
onToggle={(open) => setDropdownOpen(open || false)}
|
||||
renderTitle={
|
||||
<button
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<HiEllipsisVertical className="h-4 w-4" />
|
||||
</button>
|
||||
}
|
||||
<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)
|
||||
}}
|
||||
>
|
||||
{dropdownList}
|
||||
</Dropdown>
|
||||
<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 */}
|
||||
<div className="flex justify-center mb-3">{getFileIcon(item)}</div>
|
||||
{/* File/Folder Icon or Preview */}
|
||||
<div className="flex justify-center mb-3">
|
||||
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
|
||||
<div className="w-16 h-16">
|
||||
<ImagePreview src={`/api/app/file-management/${item.id}/download-file`} alt={item.name} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center">
|
||||
{getFileIcon(item, true)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File/Folder Name */}
|
||||
{/* 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>
|
||||
|
||||
{/* File Size */}
|
||||
{item.type === 'file' && item.size && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{formatFileSize(item.size)}</p>
|
||||
{/* File Size and Type */}
|
||||
{item.type === 'file' && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{getFileTypeLabel(item)}
|
||||
</p>
|
||||
{item.size && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatFileSize(item.size)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Folder Child Count */}
|
||||
|
|
@ -212,7 +427,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
|||
'childCount' in item &&
|
||||
typeof (item as any).childCount === 'number' && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{(item as any).childCount} items
|
||||
{(item as any).childCount} öğe
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue