erp-platform/ui/src/views/admin/files/components/FileUploadModal.tsx

379 lines
13 KiB
TypeScript
Raw Normal View History

2025-10-26 00:19:18 +00:00
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 {
2025-10-26 00:19:18 +00:00
id: string
file: File
2025-10-26 00:19:18 +00:00
progress: number
status: 'pending' | 'uploading' | 'completed' | 'error'
error?: string
errorDetail?: string
2025-10-26 00:19:18 +00:00
}
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'
2025-10-26 00:19:18 +00:00
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,
2025-10-26 00:19:18 +00:00
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
2025-10-26 00:19:18 +00:00
setUploadFiles((prev) =>
prev.map((f) =>
f.id === fileData.id ? { ...f, status: 'completed' as const, progress: 100 } : f,
),
2025-10-26 00:19:18 +00:00
)
// 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
2025-10-26 00:19:18 +00:00
}
// Mark as error with detailed message
2025-10-26 00:19:18 +00:00
setUploadFiles((prev) =>
prev.map((f) =>
f.id === fileData.id
? {
...f,
status: 'error' as const,
error: errorMessage,
errorDetail: detailMessage,
progress: 0,
}
: f,
),
2025-10-26 00:19:18 +00:00
)
}
}
2025-10-26 00:19:18 +00:00
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)
}
2025-10-26 00:19:18 +00:00
}
}
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'))
}
2025-10-26 00:19:18 +00:00
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
2025-10-26 00:19:18 +00:00
return (
<Dialog isOpen={isOpen} onClose={handleClose} className={className}>
<div ref={ref}>
<div className="flex items-center justify-between pb-2 border-b">
2025-10-26 00:19:18 +00:00
<h3 className="text-lg font-semibold">Upload Files</h3>
</div>
<div className="py-2">
2025-10-26 00:19:18 +00:00
<div
className={classNames(
'relative border-2 border-dashed rounded-lg p-4 text-center transition-colors',
2025-10-26 00:19:18 +00:00
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}
2025-10-26 00:19:18 +00:00
</p>
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
{formatFileSize(file.file.size)}
</span>
2025-10-26 00:19:18 +00:00
</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>
2025-10-26 00:19:18 +00:00
)}
</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>
))}
2025-10-26 00:19:18 +00:00
</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)`}
2025-10-26 00:19:18 +00:00
</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>
2025-10-26 00:19:18 +00:00
)}
</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>
)}
2025-10-26 00:19:18 +00:00
<Button variant="default" onClick={handleClose} disabled={uploading}>
{uploading ? 'Uploading...' : 'Close'}
2025-10-26 00:19:18 +00:00
</Button>
<Button
variant="solid"
onClick={handleUpload}
disabled={pendingFiles === 0 || uploading}
2025-10-26 00:19:18 +00:00
loading={uploading}
>
{uploading ? 'Uploading...' : `Upload ${pendingFiles} File(s)`}
2025-10-26 00:19:18 +00:00
</Button>
</div>
</div>
</div>
</Dialog>
)
})
FileUploadModal.displayName = 'FileUploadModal'
export default FileUploadModal