@spaced-out/ui-design-system
Version:
Sense UI components library
305 lines (277 loc) • 8.76 kB
Flow
// @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<FileObject>,
rejectedFiles: Array<FileObject>,
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<FileObject>,
rejectedFiles: Array<FileObject>,
};
type Action =
| {type: 'ADD_VALID_FILES', files: Array<FileObject>}
| {type: 'ADD_REJECTED_FILES', files: Array<FileObject>}
| {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<File>) => {
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<FileRejection>) => {
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,
};
};