You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
296 lines
8.4 KiB
296 lines
8.4 KiB
import { Box, Input, InputLabel } from '@mui/material'; |
|
import EventEmitter from 'events'; |
|
import { |
|
ChangeEventHandler, |
|
FormEventHandler, |
|
ReactElement, |
|
useEffect, |
|
useRef, |
|
useState, |
|
} from 'react'; |
|
|
|
import { UPLOAD_FILE_TYPES } from '../../lib/consts/UPLOAD_FILE_TYPES'; |
|
|
|
import api from '../../lib/api'; |
|
import { ProgressBar } from '../Bars'; |
|
import ContainedButton from '../ContainedButton'; |
|
import FileInfo from './FileInfo'; |
|
import MessageBox from '../MessageBox'; |
|
import { BodyText } from '../Text'; |
|
|
|
type FileUploadFormProps = { |
|
onFileUploadComplete?: () => void; |
|
eventEmitter?: EventEmitter; |
|
}; |
|
|
|
type SelectedFile = Pick< |
|
FileDetailMetadata, |
|
'fileName' | 'fileLocations' | 'fileType' |
|
> & { |
|
file: File; |
|
}; |
|
|
|
type InUploadFile = Pick<FileDetailMetadata, 'fileName'> & { |
|
progressValue: number; |
|
}; |
|
|
|
const FILE_UPLOAD_FORM_DEFAULT_PROPS: Partial<FileUploadFormProps> = { |
|
onFileUploadComplete: undefined, |
|
eventEmitter: undefined, |
|
}; |
|
|
|
const FILE_UPLOAD_PERCENT_MAX = 99; |
|
|
|
const FileUploadForm = ( |
|
{ |
|
onFileUploadComplete, |
|
eventEmitter, |
|
}: FileUploadFormProps = FILE_UPLOAD_FORM_DEFAULT_PROPS as FileUploadFormProps, |
|
): JSX.Element => { |
|
const selectFileRef = useRef<HTMLInputElement>(); |
|
|
|
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]); |
|
const [inUploadFiles, setInUploadFiles] = useState< |
|
(InUploadFile | undefined)[] |
|
>([]); |
|
|
|
const convertMIMETypeToFileTypeKey = (fileMIMEType: string): FileType => { |
|
const fileTypesIterator = UPLOAD_FILE_TYPES.entries(); |
|
|
|
let fileType: FileType | undefined; |
|
|
|
do { |
|
const fileTypesResult = fileTypesIterator.next(); |
|
|
|
if (fileTypesResult.value) { |
|
const [fileTypeKey, [mimeTypeToUse]] = fileTypesResult.value; |
|
|
|
if (fileMIMEType === mimeTypeToUse) { |
|
fileType = fileTypeKey; |
|
} |
|
} else { |
|
fileType = 'other'; |
|
} |
|
} while (!fileType); |
|
|
|
return fileType; |
|
}; |
|
|
|
const autocompleteAfterSelectFile: ChangeEventHandler<HTMLInputElement> = ({ |
|
target: { files }, |
|
}) => { |
|
if (files) { |
|
setSelectedFiles( |
|
Array.from(files).map( |
|
(file): SelectedFile => ({ |
|
file, |
|
fileName: file.name, |
|
fileLocations: [], |
|
fileType: convertMIMETypeToFileTypeKey(file.type), |
|
}), |
|
), |
|
); |
|
} |
|
}; |
|
|
|
const generateFileInfoOnChangeHandler = |
|
(fileIndex: number): FileInfoChangeHandler => |
|
(inputValues) => { |
|
selectedFiles[fileIndex] = { |
|
...selectedFiles[fileIndex], |
|
...inputValues, |
|
}; |
|
}; |
|
|
|
const uploadFiles: FormEventHandler<HTMLFormElement> = (event) => { |
|
event.preventDefault(); |
|
|
|
while (selectedFiles.length > 0) { |
|
const selectedFile = selectedFiles.shift(); |
|
|
|
if (selectedFile) { |
|
const { file, fileName } = selectedFile; |
|
|
|
const fileFormData = new FormData(); |
|
|
|
fileFormData.append('file', new File([file], fileName, { ...file })); |
|
// Re-add when the back-end tools can support changing file type on file upload. |
|
// Note: get file type from destructuring selectedFile. |
|
// fileFormData.append('file-type', fileType); |
|
|
|
const inUploadFile: InUploadFile = { fileName, progressValue: 0 }; |
|
|
|
inUploadFiles.push(inUploadFile); |
|
|
|
api |
|
.post('/file', fileFormData, { |
|
headers: { |
|
'Content-Type': 'multipart/form-data', |
|
}, |
|
onUploadProgress: ( |
|
(fName: string) => |
|
({ loaded, total }) => { |
|
setInUploadFiles((previous) => { |
|
const fInfo = previous.find((f) => f?.fileName === fName); |
|
|
|
if (!fInfo) return previous; |
|
|
|
/** |
|
* Use 99 as upper limit because progress event doesn't |
|
* represent the "bytes out of total" written to the remote |
|
* disk. The write completes when the request completes. |
|
*/ |
|
fInfo.progressValue = Math.round( |
|
(loaded / total) * FILE_UPLOAD_PERCENT_MAX, |
|
); |
|
|
|
return [...previous]; |
|
}); |
|
} |
|
)(fileName), |
|
}) |
|
.then( |
|
((fName: string) => () => { |
|
setInUploadFiles((previous) => { |
|
const fInfo = previous.find((f) => f?.fileName === fName); |
|
|
|
if (!fInfo) return previous; |
|
|
|
fInfo.progressValue = 100; |
|
|
|
return [...previous]; |
|
}); |
|
|
|
setTimeout(() => { |
|
setInUploadFiles((previous) => { |
|
const fIndex = previous.findIndex( |
|
(f) => f?.fileName === fName, |
|
); |
|
|
|
if (fIndex === -1) return previous; |
|
|
|
delete previous[fIndex]; |
|
|
|
return [...previous]; |
|
}); |
|
}, 5000); |
|
|
|
onFileUploadComplete?.call(null); |
|
})(fileName), |
|
); |
|
} |
|
} |
|
|
|
// Clears "staging area" (selected files) and populates "in-progress area" (in-upload files). |
|
setSelectedFiles([]); |
|
setInUploadFiles([...inUploadFiles]); |
|
}; |
|
|
|
useEffect(() => { |
|
eventEmitter?.addListener('openFilePicker', () => { |
|
selectFileRef.current?.click(); |
|
}); |
|
|
|
eventEmitter?.addListener('clearSelectedFiles', () => { |
|
setSelectedFiles([]); |
|
}); |
|
}, [eventEmitter]); |
|
|
|
return ( |
|
<form onSubmit={uploadFiles}> |
|
<InputLabel htmlFor="select-file"> |
|
<Input |
|
id="select-file" |
|
inputProps={{ multiple: true }} |
|
onChange={autocompleteAfterSelectFile} |
|
ref={selectFileRef} |
|
sx={{ display: 'none' }} |
|
type="file" |
|
/> |
|
</InputLabel> |
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}> |
|
{inUploadFiles.reduce<ReactElement[]>((previous, upFile) => { |
|
if (!upFile) return previous; |
|
|
|
const { fileName, progressValue } = upFile; |
|
|
|
previous.push( |
|
<Box |
|
key={`in-upload-${fileName}`} |
|
sx={{ |
|
alignItems: { md: 'center' }, |
|
display: 'flex', |
|
flexDirection: { xs: 'column', md: 'row' }, |
|
'& > :first-child': { |
|
minWidth: 100, |
|
overflow: 'hidden', |
|
overflowWrap: 'normal', |
|
textOverflow: 'ellipsis', |
|
whiteSpace: 'nowrap', |
|
width: { xs: '100%', md: 200 }, |
|
wordBreak: 'keep-all', |
|
}, |
|
'& > :last-child': { flexGrow: 1 }, |
|
}} |
|
> |
|
<BodyText text={fileName} /> |
|
<ProgressBar progressPercentage={progressValue} /> |
|
</Box>, |
|
); |
|
|
|
return previous; |
|
}, [])} |
|
</Box> |
|
<Box |
|
sx={{ |
|
display: 'flex', |
|
flexDirection: 'column', |
|
'& > :not(:first-child)': { marginTop: '1em' }, |
|
}} |
|
> |
|
{selectedFiles.length > 0 && ( |
|
<MessageBox type="info"> |
|
Uploaded files will be listed automatically, but it may take a while |
|
for larger files to finish uploading and appear on the list. |
|
</MessageBox> |
|
)} |
|
{selectedFiles.map( |
|
( |
|
{ |
|
file: { name: originalFileName }, |
|
fileName, |
|
// Re-add when the back-end tools can support changing file type on file upload. |
|
// Note: file type must be supplied to FileInfo. |
|
// fileType, |
|
fileLocations, |
|
}, |
|
fileIndex, |
|
) => ( |
|
<FileInfo |
|
{...{ fileName, fileLocations }} |
|
// Use a non-changing key to prevent recreating the component. |
|
// fileName holds the string from the file-name input, thus it changes when users makes a change. |
|
key={`selected-${originalFileName}`} |
|
onChange={generateFileInfoOnChangeHandler(fileIndex)} |
|
/> |
|
), |
|
)} |
|
{selectedFiles.length > 0 && ( |
|
<Box |
|
sx={{ |
|
display: 'flex', |
|
flexDirection: 'row', |
|
justifyContent: 'flex-end', |
|
}} |
|
> |
|
<ContainedButton type="submit">Upload</ContainedButton> |
|
</Box> |
|
)} |
|
</Box> |
|
</form> |
|
); |
|
}; |
|
|
|
FileUploadForm.defaultProps = FILE_UPLOAD_FORM_DEFAULT_PROPS; |
|
|
|
export default FileUploadForm;
|
|
|