import React, { useRef, useState, useCallback, useMemo, useId, useEffect, } from 'react' import Block from '../Primitives/Block/Block' import Text from '../Text/Text' import { Tooltip, TooltipSize } from '../Tooltip' import { HelpCircle } from 'lucide-react' import { UploadState } from './types' import type { UploadProps, UploadedFileWithStatus, UploadFile, FileRejection, UploadFormValue, } from './types' import type { UploadTokenType } from './upload.tokens' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { useUploadState, updateDragState, createDragHandlers, createClickHandler, createFileInputChangeHandler, getUploadContent, getVisualUploadState, createEnhancedValidator, createUploadManager, createComplexDropHandler, createFileRemovalHandler, createFileReplacementHandler, getMbfromKb, } from './utils' import { UploadingState, SuccessState, MixedState, ErrorState, DefaultState, } from './components' const Upload: React.FC = ({ multiple = false, accept = [], maxSize, maxFiles = multiple ? undefined : 1, disabled = false, required = false, label, subLabel, helpIconHintText, children, description, className, errorText, state: externalState, uploadingFiles: externalUploadingFiles, uploadedFiles: externalUploadedFiles, failedFiles: externalFailedFiles, value: controlledValue, onChange, onDrop, onDropAccepted, onDropRejected, onFileRemove, onReplaceFile, isDragActive: controlledIsDragActive, isDragAccept: controlledIsDragAccept, isDragReject: controlledIsDragReject, validator, progressSpeed = 200, onStateChange, ...rest }) => { const uploadTokens = useResponsiveTokens('UPLOAD') const fileInputRef = useRef(null) const uploadId = useId() const labelId = label ? `${uploadId}-label` : undefined const descriptionId = description ? `${uploadId}-description` : undefined const hintId = helpIconHintText ? `${uploadId}-hint` : undefined // Internal state management const [internalState, setInternalState] = useState( UploadState.IDLE ) const [internalUploadingFiles, setInternalUploadingFiles] = useState< UploadFile[] >([]) const [internalUploadedFiles, setInternalUploadedFiles] = useState< UploadedFileWithStatus[] >([]) const [internalFailedFiles, setInternalFailedFiles] = useState< UploadedFileWithStatus[] >([]) const getFilesFromValue = useCallback( (value: UploadFormValue | undefined): File[] => { if (!value) return [] if (Array.isArray(value)) return value return [value] }, [] ) const valueFiles = useMemo(() => { return getFilesFromValue(controlledValue) }, [controlledValue, getFilesFromValue]) const state = externalState ?? internalState const uploadingFiles = externalUploadingFiles ?? internalUploadingFiles const uploadedFiles = controlledValue ? valueFiles.map((file, index) => ({ file, id: `value-${index}-${file.name}`, status: 'success' as const, })) : (externalUploadedFiles ?? internalUploadedFiles) const failedFiles = externalFailedFiles ?? internalFailedFiles const { internalDragState, setInternalDragState, setDragCounter } = useUploadState() const isDragActive = controlledIsDragActive ?? internalDragState.isDragActive const isDragAccept = controlledIsDragAccept ?? internalDragState.isDragAccept const isDragReject = controlledIsDragReject ?? internalDragState.isDragReject const visualState = getVisualUploadState( disabled, isDragReject, isDragAccept, isDragActive, state ) const validateFile = useMemo( () => createEnhancedValidator(accept, maxSize, validator), [accept, maxSize, validator] ) const uploadManager = useMemo(() => createUploadManager(), []) useEffect(() => { return () => { uploadManager.cancelAllUploads() } }, [uploadManager]) const handleInternalDrop = useMemo( () => createComplexDropHandler( multiple, maxFiles, disabled, uploadingFiles, uploadedFiles, failedFiles, setInternalState, setInternalUploadingFiles, setInternalUploadedFiles, setInternalFailedFiles, uploadManager, progressSpeed ), [ multiple, maxFiles, disabled, validateFile, uploadingFiles, uploadedFiles, failedFiles, uploadManager, progressSpeed, ] ) const handleInternalFileRemove = useMemo( () => createFileRemovalHandler( setInternalUploadedFiles, setInternalFailedFiles, setInternalState ), [] ) const handleFileRemoveWithOnChange = useCallback( (fileId: string) => { // Check if this is a failed file const isFailedFile = failedFiles.some((f) => f.id === fileId) if (controlledValue && onChange && !isFailedFile) { // Only handle successful files in controlled mode // Failed files are not part of the controlled value const currentFiles = getFilesFromValue(controlledValue) const fileToRemove = uploadedFiles.find((f) => f.id === fileId) if (fileToRemove) { const remainingFiles = currentFiles.filter( (f) => f !== fileToRemove.file ) const formValue: UploadFormValue = multiple ? remainingFiles : remainingFiles[0] || null onChange(formValue) } } else { // For failed files or when not using controlled mode, use internal handler // This ensures failed files can always be removed handleInternalFileRemove(fileId) } onFileRemove?.(fileId) }, [ controlledValue, onChange, getFilesFromValue, uploadedFiles, failedFiles, multiple, handleInternalFileRemove, onFileRemove, ] ) const handleInternalReplaceFile = useMemo( () => createFileReplacementHandler( setInternalState, setInternalUploadedFiles, setInternalFailedFiles ), [] ) const finalOnDrop = onDrop || handleInternalDrop const finalOnFileRemove = onChange ? handleFileRemoveWithOnChange : onFileRemove || handleInternalFileRemove const finalOnReplaceFile = onReplaceFile || handleInternalReplaceFile const processFilesFn = useCallback( (files: FileList) => { if (disabled) return const fileArray = Array.from(files) const acceptedFiles: File[] = [] const rejectedFiles: FileRejection[] = [] fileArray.forEach((file) => { const errors = validateFile(file) if (errors.length === 0) { acceptedFiles.push(file) } else { rejectedFiles.push({ file, errors }) } }) if (rejectedFiles.length > 0) { onDropRejected?.(rejectedFiles) } if (acceptedFiles.length > 0) { if (onChange) { if (multiple) { const existingFiles = getFilesFromValue(controlledValue) const allFiles = [...existingFiles, ...acceptedFiles] onChange(allFiles) } else { onChange(acceptedFiles[0] || null) } } onDropAccepted?.(acceptedFiles) } else if (rejectedFiles.length > 0 && onChange && !multiple) { onChange(null) } if (finalOnDrop) { finalOnDrop(acceptedFiles, rejectedFiles) } }, [ disabled, validateFile, finalOnDrop, onDropAccepted, onDropRejected, onChange, multiple, controlledValue, getFilesFromValue, ] ) const updateDragStateFn = updateDragState( controlledIsDragActive, validateFile, setInternalDragState ) const { handleDragEnter, handleDragLeave, handleDragOver, handleDrop } = createDragHandlers( disabled, setDragCounter, updateDragStateFn, processFilesFn ) const handleClick = createClickHandler( disabled, fileInputRef as React.RefObject ) const handleFileInputChange = createFileInputChangeHandler(processFilesFn) const uploadContent = getUploadContent( uploadingFiles, uploadedFiles, failedFiles, state, multiple ) const hasError = !!errorText || (uploadContent.hasErrorFiles && uploadContent.errorFiles.length > 0) const errorId = hasError ? `${uploadId}-error` : undefined const errorFileIdsString = uploadContent.errorFiles .map((f) => f.id) .sort() .join(',') || '' const successfulFileIdsString = uploadContent.successfulFiles .map((f) => f.id) .sort() .join(',') || '' const prevStateInfoRef = useRef<{ state: UploadState hasError: boolean hasSuccess: boolean hasUploading: boolean errorFileIds: string successfulFileIds: string } | null>(null) useEffect(() => { if (!onStateChange) return const prev = prevStateInfoRef.current const currentHasSuccess = uploadContent.isSuccess && uploadContent.hasSuccessfulFiles && !hasError const currentHasUploading = uploadContent.hasUploadingFiles && uploadContent.uploadingFile !== null const hasChanged = !prev || prev.state !== state || prev.hasError !== hasError || prev.hasSuccess !== currentHasSuccess || prev.hasUploading !== currentHasUploading || prev.errorFileIds !== errorFileIdsString || prev.successfulFileIds !== successfulFileIdsString if (hasChanged) { prevStateInfoRef.current = { state, hasError, hasSuccess: currentHasSuccess, hasUploading: currentHasUploading, errorFileIds: errorFileIdsString, successfulFileIds: successfulFileIdsString, } try { onStateChange({ state, hasError, hasSuccess: currentHasSuccess, hasUploading: currentHasUploading, errorFiles: uploadContent.errorFiles || [], successfulFiles: uploadContent.successfulFiles || [], }) } catch (error) { console.error('Error in onStateChange callback:', error) } } }, [ onStateChange, state, hasError, uploadContent.isSuccess, uploadContent.hasSuccessfulFiles, uploadContent.hasUploadingFiles, uploadContent.uploadingFile, errorFileIdsString, successfulFileIdsString, ]) const renderContent = () => { if (uploadContent.hasUploadingFiles && uploadContent.uploadingFile) { return ( ) } if ( multiple && uploadContent.hasSuccessfulFiles && uploadContent.hasErrorFiles ) { return ( ) } if (uploadContent.isSuccess && uploadContent.hasSuccessfulFiles) { return ( ) } if (uploadContent.isError && uploadContent.hasErrorFiles) { return ( ) } return ( ) } return ( {label && ( {label} {required && ( * )} {subLabel && ( ({subLabel}) {helpIconHintText && ( )} )} )} {renderContent()} {description && ( {description} )} {(errorText || uploadContent.hasErrorFiles) && ( {errorText || (uploadContent.hasErrorFiles && uploadContent.errorFiles.length > 0 ? `${uploadContent.errorFiles.length} file(s) failed to upload` : 'Upload failed')} )} ) } Upload.displayName = 'Upload' export default Upload