erp-platform/ui/src/views/admin/files/components/FileUploadModal.tsx
Sedat Öztürk 697c7c1d65 Dosya Yöneticisi
File exists, Toolbar ve style güncellemeleri
2025-10-26 19:27:19 +03:00

378 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { forwardRef, useState, useCallback, useRef } from 'react'
import { Dialog, Button, Progress } from '@/components/ui'
import { HiCloudArrowUp, HiXMark } from 'react-icons/hi2'
import classNames from 'classnames'
export interface FileUploadModalProps {
isOpen: boolean
onClose: () => void
onUpload: (files: File[]) => Promise<void>
currentFolderId?: string
loading?: boolean
className?: string
}
interface UploadFileWithProgress {
id: string
file: File
progress: number
status: 'pending' | 'uploading' | 'completed' | 'error'
error?: string
errorDetail?: string
}
const FileUploadModal = forwardRef<HTMLDivElement, FileUploadModalProps>((props, ref) => {
const { isOpen, onClose, onUpload, currentFolderId, loading = false, className } = props
const [uploadFiles, setUploadFiles] = useState<UploadFileWithProgress[]>([])
const [isDragOver, setIsDragOver] = useState(false)
const [uploading, setUploading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const generateFileId = () => Math.random().toString(36).substr(2, 9)
const formatFileSize = (bytes: number): string => {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
// Handle undefined, null, NaN values
if (!bytes || bytes === 0 || isNaN(bytes)) return '0 B'
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i]
}
const handleFilesSelect = useCallback((files: FileList | File[]) => {
const fileArray = Array.from(files)
const newFiles: UploadFileWithProgress[] = fileArray.map((file) => ({
id: generateFileId(),
file: file,
progress: 0,
status: 'pending' as const,
}))
setUploadFiles((prev) => [...prev, ...newFiles])
}, [])
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
handleFilesSelect(e.target.files)
}
}
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(true)
}, [])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
}, [])
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
if (e.dataTransfer.files) {
handleFilesSelect(e.dataTransfer.files)
}
},
[handleFilesSelect],
)
const removeFile = (fileId: string) => {
setUploadFiles((prev) => prev.filter((f) => f.id !== fileId))
}
const handleUpload = async () => {
if (uploadFiles.length === 0) return
setUploading(true)
const filesToUpload = uploadFiles.filter((f) => f.status === 'pending')
// Upload files one by one
for (const fileData of filesToUpload) {
let progressInterval: NodeJS.Timeout | null = null
try {
// Set status to uploading
setUploadFiles((prev) =>
prev.map((f) => (f.id === fileData.id ? { ...f, status: 'uploading' as const } : f)),
)
// Simulate progress for visual feedback
progressInterval = setInterval(() => {
setUploadFiles((prev) =>
prev.map((f) => {
if (f.id === fileData.id && f.progress < 90) {
return { ...f, progress: f.progress + 10 }
}
return f
}),
)
}, 100)
// Call the actual upload function for single file
await onUpload([fileData.file])
// Clear progress interval
if (progressInterval) {
clearInterval(progressInterval)
progressInterval = null
}
// Mark as completed and remove from list after delay
setUploadFiles((prev) =>
prev.map((f) =>
f.id === fileData.id ? { ...f, status: 'completed' as const, progress: 100 } : f,
),
)
// Remove completed files from list after 2 seconds
setTimeout(() => {
setUploadFiles((prev) => prev.filter((f) => f.id !== fileData.id))
}, 2000)
} catch (error: any) {
console.error('Upload failed for file:', fileData.file.name, error)
// Clear progress interval in case of error
if (progressInterval) {
clearInterval(progressInterval)
progressInterval = null
}
// Extract detailed error message from ABP response
let errorMessage = 'Upload failed'
let detailMessage = ''
if (error?.response?.data?.error) {
const errorData = error.response.data.error
// Ana hata mesajı
if (errorData.message) {
errorMessage = errorData.message
}
// Detay mesajı - validationErrors veya details'den
if (errorData.details) {
detailMessage = errorData.details
} else if (errorData.validationErrors && errorData.validationErrors.length > 0) {
detailMessage = errorData.validationErrors[0].message || errorData.validationErrors[0]
}
// Dosya boyutu kontrolü için özel mesaj
if (detailMessage.includes('Request body too large') || detailMessage.includes('max request body size')) {
const maxSizeMB = 30 // 30MB limit
errorMessage = 'File too large'
detailMessage = `File size exceeds the maximum allowed size of ${maxSizeMB}MB. Your file is ${(fileData.file.size / (1024 * 1024)).toFixed(1)}MB.`
}
} else if (error?.message) {
errorMessage = error.message
} else if (typeof error === 'string') {
errorMessage = error
}
// Mark as error with detailed message
setUploadFiles((prev) =>
prev.map((f) =>
f.id === fileData.id
? {
...f,
status: 'error' as const,
error: errorMessage,
errorDetail: detailMessage,
progress: 0,
}
: f,
),
)
}
}
setUploading(false)
// Check if all files are processed (completed or error)
const remainingFiles = uploadFiles.filter(
(f) => f.status === 'pending' || f.status === 'uploading',
)
if (remainingFiles.length === 0) {
// If no pending files and no errors, close modal
const hasErrors = uploadFiles.some((f) => f.status === 'error')
if (!hasErrors) {
setTimeout(() => {
onClose()
setUploadFiles([])
}, 2000)
}
}
}
const handleClose = () => {
if (!uploading) {
setUploadFiles([])
onClose()
}
}
const clearCompletedFiles = () => {
setUploadFiles((prev) => prev.filter((f) => f.status !== 'completed'))
}
const clearErrorFiles = () => {
setUploadFiles((prev) => prev.filter((f) => f.status !== 'error'))
}
const totalFiles = uploadFiles.length
const completedFiles = uploadFiles.filter((f) => f.status === 'completed').length
const errorFiles = uploadFiles.filter((f) => f.status === 'error').length
const pendingFiles = uploadFiles.filter((f) => f.status === 'pending').length
const hasError = errorFiles > 0
return (
<Dialog isOpen={isOpen} onClose={handleClose} className={className}>
<div ref={ref}>
<div className="flex items-center justify-between pb-2 border-b">
<h3 className="text-lg font-semibold">Upload Files</h3>
</div>
<div className="py-2">
<div
className={classNames(
'relative border-2 border-dashed rounded-lg p-4 text-center transition-colors',
isDragOver
? 'border-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400',
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<input
ref={fileInputRef}
type="file"
multiple
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleFileInputChange}
disabled={uploading}
/>
<HiCloudArrowUp className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<p className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Choose files or drag and drop here
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Select one or more files to upload
</p>
</div>
{/* File List */}
{uploadFiles.length > 0 && (
<div className="space-y-1 max-h-80 overflow-y-auto">
{uploadFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded-lg"
>
<div className="flex-1 min-w-0">
{/* File name and size in one line */}
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate flex-1 mr-2">
{file.file.name}
</p>
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
{formatFileSize(file.file.size)}
</span>
</div>
{/* Progress Bar */}
{file.status === 'uploading' && (
<div className="mt-1">
<Progress percent={file.progress} size="sm" />
</div>
)}
{/* Status Messages */}
{file.status === 'completed' && (
<p className="text-xs text-green-600 dark:text-green-400 mt-1">
Upload completed
</p>
)}
{file.status === 'error' && (
<div className="mt-1">
<p className="text-xs text-red-600 dark:text-red-400 font-medium">
{file.error || 'Upload failed'}
</p>
{file.errorDetail && (
<p className="text-xs text-red-500 dark:text-red-400 mt-0.5 leading-relaxed">
{file.errorDetail}
</p>
)}
</div>
)}
</div>
{(file.status === 'pending' || file.status === 'error') && (
<Button
variant="plain"
size="xs"
icon={<HiXMark />}
onClick={() => removeFile(file.id)}
disabled={uploading}
className="ml-2 flex-shrink-0"
/>
)}
</div>
))}
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-between items-center pt-4 border-t">
<div className="text-sm text-gray-500 dark:text-gray-400">
{uploading && (
<span>
Uploading files... {completedFiles > 0 && `(${completedFiles} completed)`}
</span>
)}
{!uploading && !hasError && pendingFiles > 0 && (
<span className="text-green-600">Ready to upload {pendingFiles} file(s)</span>
)}
{!uploading && !hasError && pendingFiles === 0 && totalFiles === 0 && (
<span className="text-gray-500">No files selected</span>
)}
</div>
<div className="flex space-x-2">
{hasError && !uploading && (
<Button
variant="plain"
onClick={clearErrorFiles}
className="text-red-600 hover:text-red-700"
>
Clear Errors
</Button>
)}
<Button variant="default" onClick={handleClose} disabled={uploading}>
{uploading ? 'Uploading...' : 'Close'}
</Button>
<Button
variant="solid"
onClick={handleUpload}
disabled={pendingFiles === 0 || uploading}
loading={uploading}
>
{uploading ? 'Uploading...' : `Upload ${pendingFiles} File(s)`}
</Button>
</div>
</div>
</div>
</Dialog>
)
})
FileUploadModal.displayName = 'FileUploadModal'
export default FileUploadModal