import { AxiosRequestConfig } from 'axios'; import { useFormik } from 'formik'; import { ChangeEventHandler, FC, ReactElement, useCallback, useMemo, useRef, useState, } from 'react'; import { v4 as uuidv4 } from 'uuid'; import ActionGroup from '../ActionGroup'; import api from '../../lib/api'; import ContainedButton from '../ContainedButton'; import FileInputGroup from './FileInputGroup'; import FlexBox from '../FlexBox'; import getFormikErrorMessages from '../../lib/getFormikErrorMessages'; import handleAPIError from '../../lib/handleAPIError'; import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; import fileListSchema from './schema'; import UploadFileProgress from './UploadFileProgress'; 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 messageGroupRef = useRef(null); const filePickerRef = useRef(null); const [uploads, setUploads] = useState(); const setApiMessage = useCallback( (msg?: Message) => messageGroupRef?.current?.setMessage?.call(null, 'api', msg), [], ); 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; }, {}), ); setApiMessage({ children: ( <> Closing this dialog before the upload(s) complete will cancel the upload(s). ), }); 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, ): AxiosRequestConfig['onUploadProgress'] => ({ 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) .then(() => { setApiMessage({ children: ( Upload(s) completed; file(s) will be listed after the job(s) to sync them to other host(s) finish. You can close this dialog. ), }); }) .catch((error) => { const emsg = handleAPIError(error); emsg.children = <>Failed to add file. {emsg.children}; setApiMessage(emsg); }); }, validationSchema: fileListSchema, }); const formikErrors = useMemo( () => getFormikErrorMessages(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 ( {uploads ? ( ) : ( { event.preventDefault(); formik.submitForm(); }} > { filePickerRef.current?.click(); }} > Browse {fileInputs} )} ); }; export default AddFileForm;