// @flow strict import * as React from 'react'; // $FlowFixMe[untyped-import] import {useDropzone} from 'react-dropzone'; import type { FileObject, FileProgress, FileRejection, FileUploadBaseProps, } from '../../components/FileUpload'; import {uuid} from '../../utils/helpers'; // useFileUpload hook returns these props export type UseFileUploadReturnProps = { validFiles: Array, rejectedFiles: Array, isDragActive: boolean, shouldAcceptFiles: boolean, getRootProps: (mixed) => mixed, getInputProps: (mixed) => mixed, handleFileClear: (id: string) => mixed, handleClear: () => mixed, moveFileToProgress: (id: string, progress: FileProgress) => mixed, moveFileToSuccess: (id: string, successMessage?: string) => mixed, moveFileToReject: (id: string, rejectReason?: string) => mixed, setShowReUpload: (id: string, showReUpload?: boolean) => mixed, }; type State = { validFiles: Array, rejectedFiles: Array, }; type Action = | {type: 'ADD_VALID_FILES', files: Array} | {type: 'ADD_REJECTED_FILES', files: Array} | {type: 'REMOVE_FILE', id: string} | {type: 'CLEAR_FILES'} | {type: 'UPDATE_FILE_PROGRESS', id: string, progress: FileProgress} | {type: 'SET_FILE_SUCCESS', id: string, successMessage?: string} | {type: 'SET_FILE_REJECT', id: string, rejectReason?: string} | {type: 'SET_FILE_RE_UPLOAD', id: string, showReUpload?: boolean}; const initialState: State = { validFiles: [], rejectedFiles: [], }; const reducer = (state: State, action: Action): State => { switch (action.type) { case 'ADD_VALID_FILES': return { ...state, validFiles: [...state.validFiles, ...action.files], }; case 'ADD_REJECTED_FILES': return { ...state, rejectedFiles: [...state.rejectedFiles, ...action.files], }; case 'REMOVE_FILE': return { ...state, validFiles: state.validFiles.filter((file) => file.id !== action.id), rejectedFiles: state.rejectedFiles.filter( (file) => file.id !== action.id, ), }; case 'CLEAR_FILES': return { validFiles: [], rejectedFiles: [], }; case 'UPDATE_FILE_PROGRESS': return { ...state, validFiles: state.validFiles.map((file) => file.id === action.id ? { ...file, progress: action.progress, showReUpload: false, successMessage: undefined, rejectReason: undefined, } : file, ), rejectedFiles: state.rejectedFiles.map((file) => file.id === action.id ? { ...file, progress: action.progress, showReUpload: false, successMessage: undefined, rejectReason: undefined, } : file, ), }; case 'SET_FILE_RE_UPLOAD': return { ...state, validFiles: state.validFiles.map((file) => file.id === action.id ? {...file, progress: undefined, showReUpload: action.showReUpload} : file, ), rejectedFiles: state.rejectedFiles.map((file) => file.id === action.id ? {...file, progress: undefined, showReUpload: action.showReUpload} : file, ), }; case 'SET_FILE_SUCCESS': { // Note: When a file is accepted manually we would move a file from rejected files(if found) to valid files first const fileIndex = state.rejectedFiles.findIndex( (file) => file.id === action.id, ); let validFiles = [...state.validFiles]; const rejectedFiles = [...state.rejectedFiles]; if (fileIndex !== -1) { const file = rejectedFiles[fileIndex]; rejectedFiles.splice(fileIndex, 1); validFiles = [...validFiles, {...file}]; } validFiles = validFiles.map((file) => file.id === action.id ? { ...file, success: true, successMessage: action.successMessage, reject: false, rejectReason: undefined, progress: undefined, showReUpload: false, } : file, ); return { ...state, validFiles, rejectedFiles, }; } case 'SET_FILE_REJECT': { // Note: When a file is rejected manually we would move a file from valid files(if found) to rejected files first const fileIndex = state.validFiles.findIndex( (file) => file.id === action.id, ); const validFiles = [...state.validFiles]; let rejectedFiles = [...state.rejectedFiles]; if (fileIndex !== -1) { const file = validFiles[fileIndex]; validFiles.splice(fileIndex, 1); rejectedFiles = [...rejectedFiles, {...file}]; } rejectedFiles = rejectedFiles.map((file) => file.id === action.id ? { ...file, reject: true, rejectReason: action.rejectReason, success: false, successMessage: undefined, progress: undefined, showReUpload: false, } : file, ); return { ...state, validFiles, rejectedFiles, }; } default: return state; } }; // This is a map of error codes returned by react-dropzone and their corresponding error messages const DROPZONE_ERROR_MESSAGES = { 'file-too-large': 'File exceeds maximum size', 'file-invalid-type': 'Wrong file type', 'too-many-files': 'Too many files', 'file-too-small': 'File is too small', }; export const useFileUpload = ({ maxFiles = 1, maxSize, accept, disabled, onValidFilesDrop, onRejectedFilesDrop, onFileClear, onClear, }: FileUploadBaseProps): UseFileUploadReturnProps => { const [state, dispatch] = React.useReducer(reducer, initialState); // Callbacks for when files are dropped / selected and are valid const handleDropAccepted = (acceptedFiles: Array) => { const validFiles = acceptedFiles.map((file) => ({ file, id: uuid(), reject: false, rejectReason: undefined, success: true, successMessage: undefined, progress: undefined, showReUpload: false, })); dispatch({type: 'ADD_VALID_FILES', files: validFiles}); onValidFilesDrop?.(validFiles); }; // Callbacks for when files are dropped / selected and are invalid const handleDropRejected = (fileRejections: Array) => { const rejectedFiles = fileRejections.map(({file, errors}) => ({ file, id: uuid(), reject: true, rejectReason: DROPZONE_ERROR_MESSAGES[errors[0].code] || 'Some error occurred uploading this file', success: false, successMessage: undefined, progress: undefined, showReUpload: false, })); dispatch({type: 'ADD_REJECTED_FILES', files: rejectedFiles}); onRejectedFilesDrop?.(rejectedFiles); }; const handleFileClear = (id: string) => { dispatch({type: 'REMOVE_FILE', id}); onFileClear?.(id); }; const handleClear = () => { dispatch({type: 'CLEAR_FILES'}); onClear?.(); }; const moveFileToProgress = (id: string, progress: FileProgress) => { dispatch({type: 'UPDATE_FILE_PROGRESS', id, progress}); }; // Note(Nishant): If the file is found in rejected files, we move it to valid files first in the reducer const moveFileToSuccess = (id: string, successMessage?: string) => { dispatch({type: 'SET_FILE_SUCCESS', id, successMessage}); }; // Note(Nishant): If the file is found in valid files, we move it to rejected files first in the reducer const moveFileToReject = (id: string, rejectReason?: string) => { dispatch({type: 'SET_FILE_REJECT', id, rejectReason}); }; // Note: This is used to show the re-upload button on the file const setShowReUpload = (id: string, showReUpload?: boolean) => { dispatch({type: 'SET_FILE_RE_UPLOAD', id, showReUpload}); }; const totalFiles = state.validFiles.length + state.rejectedFiles.length; const shouldAcceptFiles = !disabled && (maxFiles === 0 || totalFiles < maxFiles); // We are using react-dropzone's useDropzone hook to get the drag and drop props const {isDragActive, getRootProps, getInputProps} = useDropzone({ maxFiles, multiple: maxFiles > 1 || maxFiles === 0, maxSize, accept, disabled, noClick: !shouldAcceptFiles, noDrag: !shouldAcceptFiles, onDropAccepted: handleDropAccepted, onDropRejected: handleDropRejected, }); return { validFiles: state.validFiles, rejectedFiles: state.rejectedFiles, shouldAcceptFiles, isDragActive, getRootProps, getInputProps, handleFileClear, handleClear, moveFileToProgress, moveFileToSuccess, moveFileToReject, setShowReUpload, }; };