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.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;
|
||||||
|
|
@ -200,42 +199,50 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
{
|
{
|
||||||
ValidateFileName(input.Name);
|
ValidateFileName(input.Name);
|
||||||
|
|
||||||
var items = await GetFolderIndexAsync(input.ParentId);
|
var cdnBasePath = _configuration["App:CdnPath"];
|
||||||
|
if (string.IsNullOrEmpty(cdnBasePath))
|
||||||
if (items.Any(x => x.Name.Equals(input.Name, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
throw new UserFriendlyException("A folder or file with this name already exists");
|
throw new UserFriendlyException("CDN path is not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
var folderId = GenerateFileId();
|
var tenantId = _currentTenant.Id?.ToString() ?? "host";
|
||||||
var folderPath = string.IsNullOrEmpty(input.ParentId)
|
var parentPath = Path.Combine(cdnBasePath, tenantId);
|
||||||
? input.Name
|
|
||||||
: $"{input.ParentId}/{input.Name}";
|
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
|
var metadata = new FileMetadata
|
||||||
{
|
{
|
||||||
Id = folderId,
|
Id = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}",
|
||||||
Name = input.Name,
|
Name = input.Name,
|
||||||
Type = "folder",
|
Type = "folder",
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
ModifiedAt = DateTime.UtcNow,
|
ModifiedAt = DateTime.UtcNow,
|
||||||
Path = folderPath,
|
Path = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}",
|
||||||
ParentId = input.ParentId ?? string.Empty,
|
ParentId = input.ParentId ?? string.Empty,
|
||||||
IsReadOnly = false,
|
IsReadOnly = false,
|
||||||
TenantId = _currentTenant.Id?.ToString()
|
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
|
return new FileItemDto
|
||||||
{
|
{
|
||||||
Id = metadata.Id,
|
Id = metadata.Id,
|
||||||
|
|
@ -267,18 +274,37 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
|
|
||||||
var blobPath = GetTenantPrefix() + filePath;
|
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;
|
long fileSize;
|
||||||
if (input.FileStream != null)
|
if (input.FileStream != null)
|
||||||
{
|
{
|
||||||
input.FileStream.Position = 0;
|
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;
|
fileSize = input.FileStream.Length;
|
||||||
}
|
}
|
||||||
else if (input.FileContent != null)
|
else if (input.FileContent != null)
|
||||||
{
|
{
|
||||||
using var stream = new MemoryStream(input.FileContent);
|
await File.WriteAllBytesAsync(fullFilePath, input.FileContent);
|
||||||
await _blobContainer.SaveAsync(blobPath, stream);
|
|
||||||
fileSize = input.FileContent.Length;
|
fileSize = input.FileContent.Length;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -452,42 +478,48 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
|
|
||||||
public async Task DeleteItemAsync(string id)
|
public async Task DeleteItemAsync(string id)
|
||||||
{
|
{
|
||||||
var metadata = await FindItemMetadataAsync(id);
|
var cdnBasePath = _configuration["App:CdnPath"];
|
||||||
if (metadata == null)
|
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
|
// Klasör sil (içindeki tüm dosyalar ile birlikte)
|
||||||
await _blobContainer.DeleteAsync(blobPath + FolderMarkerSuffix);
|
Directory.Delete(fullPath, recursive: true);
|
||||||
await _blobContainer.DeleteAsync(GetTenantPrefix() + $"{id}/{IndexFileName}");
|
}
|
||||||
|
else if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
// Dosya sil
|
||||||
|
File.Delete(fullPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Delete file
|
throw new UserFriendlyException("Item not found");
|
||||||
await _blobContainer.DeleteAsync(blobPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
public async Task<Stream> DownloadFileAsync(string id)
|
||||||
{
|
{
|
||||||
var metadata = await FindItemMetadataAsync(id);
|
var cdnBasePath = _configuration["App:CdnPath"];
|
||||||
if (metadata == null || metadata.Type != "file")
|
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");
|
throw new UserFriendlyException("File not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var blobPath = GetTenantPrefix() + metadata.Path;
|
return File.OpenRead(fullFilePath);
|
||||||
return await _blobContainer.GetAsync(blobPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Stream> GetFilePreviewAsync(string id)
|
public async Task<Stream> GetFilePreviewAsync(string id)
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ public class BlobManager : DomainService
|
||||||
// Default container methods (for FileManagement and other general purposes)
|
// Default container methods (for FileManagement and other general purposes)
|
||||||
private IBlobContainer GetDefaultContainer()
|
private IBlobContainer GetDefaultContainer()
|
||||||
{
|
{
|
||||||
return _blobContainerFactory.Create("");
|
return _blobContainerFactory.Create("default");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(string blobName, Stream bytes, bool overrideExisting = true)
|
public async Task SaveAsync(string blobName, Stream bytes, bool overrideExisting = true)
|
||||||
|
|
|
||||||
|
|
@ -390,19 +390,32 @@ const FileManager = () => {
|
||||||
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
|
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Files Grid/List */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center items-center py-20">
|
<div className="flex justify-center items-center py-20">
|
||||||
<Spinner size={40} />
|
<Spinner size="lg" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<>
|
||||||
className={classNames(
|
{/* List View Header */}
|
||||||
viewMode === 'grid'
|
{viewMode === 'list' && (
|
||||||
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4'
|
<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">
|
||||||
: 'space-y-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 ? (
|
{filteredItems.length === 0 ? (
|
||||||
<div className="col-span-full text-center py-20">
|
<div className="col-span-full text-center py-20">
|
||||||
<FaFolder className="mx-auto h-16 w-16 text-gray-400 mb-4" />
|
<FaFolder className="mx-auto h-16 w-16 text-gray-400 mb-4" />
|
||||||
|
|
@ -415,17 +428,37 @@ const FileManager = () => {
|
||||||
<FileItem
|
<FileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
|
viewMode={viewMode}
|
||||||
selected={selectedItems.includes(item.id)}
|
selected={selectedItems.includes(item.id)}
|
||||||
onSelect={handleItemSelect}
|
onSelect={handleItemSelect}
|
||||||
onDoubleClick={handleItemDoubleClick}
|
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}
|
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])}
|
onDelete={(item) => openDeleteModal([item])}
|
||||||
onDownload={item.type === 'file' ? handleDownload : undefined}
|
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 */}
|
{/* Modals */}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { forwardRef, useState } from 'react'
|
import { forwardRef, useState, useEffect } from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import {
|
import {
|
||||||
HiFolder,
|
HiFolder,
|
||||||
|
HiFolderPlus,
|
||||||
HiDocument,
|
HiDocument,
|
||||||
HiPhoto,
|
HiPhoto,
|
||||||
HiFilm,
|
HiFilm,
|
||||||
|
|
@ -14,14 +15,16 @@ import {
|
||||||
HiArrowDownTray,
|
HiArrowDownTray,
|
||||||
HiEye,
|
HiEye,
|
||||||
} from 'react-icons/hi2'
|
} 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'
|
import type { FileItem as FileItemType, FileActionMenuItem } from '@/types/fileManagement'
|
||||||
|
|
||||||
export interface FileItemProps {
|
export interface FileItemProps {
|
||||||
item: FileItemType
|
item: FileItemType
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
|
viewMode?: 'grid' | 'list'
|
||||||
onSelect?: (item: FileItemType) => void
|
onSelect?: (item: FileItemType) => void
|
||||||
onDoubleClick?: (item: FileItemType) => void
|
onDoubleClick?: (item: FileItemType) => void
|
||||||
|
onCreateFolder?: (parentItem: FileItemType) => void
|
||||||
onRename?: (item: FileItemType) => void
|
onRename?: (item: FileItemType) => void
|
||||||
onMove?: (item: FileItemType) => void
|
onMove?: (item: FileItemType) => void
|
||||||
onDelete?: (item: FileItemType) => void
|
onDelete?: (item: FileItemType) => void
|
||||||
|
|
@ -30,31 +33,33 @@ export interface FileItemProps {
|
||||||
className?: string
|
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') {
|
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 extension = item.extension?.toLowerCase()
|
||||||
const mimeType = item.mimeType?.toLowerCase()
|
const mimeType = item.mimeType?.toLowerCase()
|
||||||
|
|
||||||
if (mimeType?.startsWith('image/')) {
|
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/')) {
|
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/')) {
|
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 || '')) {
|
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 => {
|
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]
|
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 FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
item,
|
item,
|
||||||
selected = false,
|
selected = false,
|
||||||
|
viewMode = 'grid',
|
||||||
onSelect,
|
onSelect,
|
||||||
onDoubleClick,
|
onDoubleClick,
|
||||||
|
onCreateFolder,
|
||||||
onRename,
|
onRename,
|
||||||
onMove,
|
onMove,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
|
@ -92,119 +136,290 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionMenuItems: FileActionMenuItem[] = [
|
const actionMenuItems: FileActionMenuItem[] = [
|
||||||
|
// Preview - sadece dosyalar için
|
||||||
...(item.type === 'file' && onPreview
|
...(item.type === 'file' && onPreview
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'preview',
|
key: 'preview',
|
||||||
label: 'Preview',
|
label: 'Önizle',
|
||||||
icon: 'HiEye',
|
icon: 'HiEye',
|
||||||
onClick: () => onPreview(item),
|
onClick: () => onPreview(item),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
// Download - sadece dosyalar için
|
||||||
...(item.type === 'file' && onDownload
|
...(item.type === 'file' && onDownload
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'download',
|
key: 'download',
|
||||||
label: 'Download',
|
label: 'İndir',
|
||||||
icon: 'HiArrowDownTray',
|
icon: 'HiArrowDownTray',
|
||||||
onClick: () => onDownload(item),
|
onClick: () => onDownload(item),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
// Create Folder - sadece klasörler için
|
||||||
key: 'rename',
|
...(item.type === 'folder' && onCreateFolder
|
||||||
label: 'Rename',
|
? [
|
||||||
icon: 'HiPencil',
|
{
|
||||||
onClick: () => onRename?.(item),
|
key: 'createFolder',
|
||||||
},
|
label: 'Yeni Klasör',
|
||||||
{
|
icon: 'HiFolderPlus',
|
||||||
key: 'move',
|
onClick: () => onCreateFolder(item),
|
||||||
label: 'Move',
|
},
|
||||||
icon: 'HiArrowRightOnRectangle',
|
]
|
||||||
onClick: () => onMove?.(item),
|
: []),
|
||||||
},
|
// Rename - her şey için
|
||||||
{
|
...(onRename
|
||||||
key: 'delete',
|
? [
|
||||||
label: 'Delete',
|
{
|
||||||
icon: 'HiTrash',
|
key: 'rename',
|
||||||
dangerous: true,
|
label: 'Yeniden Adlandır',
|
||||||
onClick: () => onDelete?.(item),
|
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 = (
|
const dropdownList = (
|
||||||
<div className="py-1">
|
<div className="py-1 min-w-36">
|
||||||
{actionMenuItems.map((menuItem) => (
|
{actionMenuItems.map((menuItem) => (
|
||||||
<div
|
<div
|
||||||
key={menuItem.key}
|
key={menuItem.key}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700',
|
'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',
|
menuItem.dangerous && 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
menuItem.onClick()
|
menuItem.onClick()
|
||||||
setDropdownOpen(false)
|
setDropdownOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-2" />}
|
{menuItem.icon === 'HiFolderPlus' && <HiFolderPlus className="h-4 w-4 mr-3 text-blue-500" />}
|
||||||
{menuItem.icon === 'HiArrowDownTray' && <HiArrowDownTray className="h-4 w-4 mr-2" />}
|
{menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-3 text-gray-500" />}
|
||||||
{menuItem.icon === 'HiPencil' && <HiPencil className="h-4 w-4 mr-2" />}
|
{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' && (
|
{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.icon === 'HiTrash' && <HiTrash className="h-4 w-4 mr-3" />}
|
||||||
{menuItem.label}
|
<span className="flex-1">{menuItem.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'relative group p-4 border rounded-lg cursor-pointer transition-all duration-200',
|
'relative group p-4 border rounded-xl cursor-pointer transition-all duration-300',
|
||||||
'hover:border-blue-300 hover:shadow-md',
|
'hover:border-blue-400 hover:shadow-lg hover:-translate-y-1',
|
||||||
selected
|
selected
|
||||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-md'
|
? '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',
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
>
|
>
|
||||||
{/* Action Menu */}
|
{/* Action Menu */}
|
||||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
|
||||||
<Dropdown
|
<button
|
||||||
onToggle={(open) => setDropdownOpen(open || false)}
|
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"
|
||||||
renderTitle={
|
onClick={(e) => {
|
||||||
<button
|
e.stopPropagation()
|
||||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
|
setDropdownOpen(!dropdownOpen)
|
||||||
onClick={(e) => {
|
}}
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HiEllipsisVertical className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{dropdownList}
|
<HiEllipsisVertical className="h-4 w-4 text-gray-600 dark:text-gray-300" />
|
||||||
</Dropdown>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* File/Folder Icon */}
|
{/* File/Folder Icon or Preview */}
|
||||||
<div className="flex justify-center mb-3">{getFileIcon(item)}</div>
|
<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">
|
<div className="text-center">
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate mb-1">
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate mb-1">
|
||||||
{item.name}
|
{item.name}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* File Size */}
|
{/* File Size and Type */}
|
||||||
{item.type === 'file' && item.size && (
|
{item.type === 'file' && (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{formatFileSize(item.size)}</p>
|
<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 */}
|
{/* Folder Child Count */}
|
||||||
|
|
@ -212,7 +427,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
|
||||||
'childCount' in item &&
|
'childCount' in item &&
|
||||||
typeof (item as any).childCount === 'number' && (
|
typeof (item as any).childCount === 'number' && (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{(item as any).childCount} items
|
{(item as any).childCount} öğe
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue