import { useFormik } from 'formik'; import { ChangeEventHandler, FC, ReactElement, useCallback, useMemo, useRef, } from 'react'; import { v4 as uuidv4 } from 'uuid'; import ActionGroup from '../ActionGroup'; import api from '../../lib/api'; import ContainedButton from '../ContainedButton'; import convertFormikErrorsToMessages from '../../lib/convertFormikErrorsToMessages'; import FileInputGroup from './FileInputGroup'; import FlexBox from '../FlexBox'; import handleAPIError from '../../lib/handleAPIError'; import MessageBox from '../MessageBox'; import MessageGroup from '../MessageGroup'; import fileListSchema from './schema'; import UploadFileProgress from './UploadFileProgress'; import useProtectedState from '../../hooks/useProtectedState'; const REQUEST_INCOMPLETE_UPLOAD_LIMIT = 99; const setUploadProgress: ( previous: UploadFiles | undefined, uuid: keyof UploadFiles, progress: UploadFiles[string]['progress'], ) => UploadFiles | undefined = (previous, uuid, progress) => { if (!previous) return previous; previous[uuid].progress = progress; return { ...previous }; }; const AddFileForm: FC = (props) => { const { anvils, drHosts } = props; const filePickerRef = useRef(null); const [uploads, setUploads] = useProtectedState( undefined, ); const formik = useFormik({ initialValues: {}, onSubmit: (values) => { const files = Object.values(values); setUploads( files.reduce((previous, { file, name, uuid }) => { if (!file) return previous; previous[uuid] = { name, progress: 0, uuid }; return previous; }, {}), ); const promises = files.reduce[]>( (chain, { file, name, uuid }) => { if (!file) return chain; const data = new FormData(); data.append('file', new File([file], name, { ...file })); const promise = api .post('/file', data, { headers: { 'Content-Type': 'multipart/form-data', }, onUploadProgress: ( (fileUuid: string) => ({ loaded, total }) => { setUploads((previous) => setUploadProgress( previous, fileUuid, Math.round( (loaded / total) * REQUEST_INCOMPLETE_UPLOAD_LIMIT, ), ), ); } )(uuid), }) .then( ((fileUuid: string) => () => { setUploads((previous) => setUploadProgress(previous, fileUuid, 100), ); })(uuid), ); chain.push(promise); return chain; }, [], ); Promise.all(promises).catch((error) => { const emsg = handleAPIError(error); emsg.children = <>Failed to add file. {emsg.children}; }); }, validationSchema: fileListSchema, }); const formikErrors = useMemo( () => convertFormikErrorsToMessages(formik.errors), [formik.errors], ); const disableProceed = useMemo( () => !formik.dirty || !formik.isValid || formik.isValidating || formik.isSubmitting, [formik.dirty, formik.isSubmitting, formik.isValid, formik.isValidating], ); const handleSelectFiles = useCallback>( (event) => { const { target: { files }, } = event; if (!files) return; const values = Array.from(files).reduce( (previous, file) => { const fileUuid = uuidv4(); previous[fileUuid] = { file, name: file.name, uuid: fileUuid, }; return previous; }, {}, ); formik.setValues(values); }, [formik], ); const fileInputs = useMemo( () => formik.values && Object.values(formik.values).map((file) => { const { uuid: fileUuid } = file; return ( ); }), [anvils, drHosts, formik], ); return ( Uploaded files will be listed automatically, but it may take a while for larger files to finish uploading and appear on the list. {uploads ? ( <> This dialog can be closed after all uploads complete. Closing before completion will stop the upload. ) : ( { event.preventDefault(); formik.submitForm(); }} > { filePickerRef.current?.click(); }} > Browse {fileInputs} )} ); }; export default AddFileForm;