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.
233 lines
5.9 KiB
233 lines
5.9 KiB
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<AddFileFormProps> = (props) => { |
|
const { anvils, drHosts } = props; |
|
|
|
const filePickerRef = useRef<HTMLInputElement>(null); |
|
|
|
const [uploads, setUploads] = useProtectedState<UploadFiles | undefined>( |
|
undefined, |
|
); |
|
|
|
const formik = useFormik<FileFormikValues>({ |
|
initialValues: {}, |
|
onSubmit: (values) => { |
|
const files = Object.values(values); |
|
|
|
setUploads( |
|
files.reduce<UploadFiles>((previous, { file, name, uuid }) => { |
|
if (!file) return previous; |
|
|
|
previous[uuid] = { name, progress: 0, uuid }; |
|
|
|
return previous; |
|
}, {}), |
|
); |
|
|
|
const promises = files.reduce<Promise<void>[]>( |
|
(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<Messages>( |
|
() => convertFormikErrorsToMessages(formik.errors), |
|
[formik.errors], |
|
); |
|
|
|
const disableProceed = useMemo<boolean>( |
|
() => |
|
!formik.dirty || |
|
!formik.isValid || |
|
formik.isValidating || |
|
formik.isSubmitting, |
|
[formik.dirty, formik.isSubmitting, formik.isValid, formik.isValidating], |
|
); |
|
|
|
const handleSelectFiles = useCallback<ChangeEventHandler<HTMLInputElement>>( |
|
(event) => { |
|
const { |
|
target: { files }, |
|
} = event; |
|
|
|
if (!files) return; |
|
|
|
const values = Array.from(files).reduce<FileFormikValues>( |
|
(previous, file) => { |
|
const fileUuid = uuidv4(); |
|
|
|
previous[fileUuid] = { |
|
file, |
|
name: file.name, |
|
uuid: fileUuid, |
|
}; |
|
|
|
return previous; |
|
}, |
|
{}, |
|
); |
|
|
|
formik.setValues(values); |
|
}, |
|
[formik], |
|
); |
|
|
|
const fileInputs = useMemo<ReactElement[]>( |
|
() => |
|
formik.values && |
|
Object.values(formik.values).map((file) => { |
|
const { uuid: fileUuid } = file; |
|
|
|
return ( |
|
<FileInputGroup |
|
anvils={anvils} |
|
drHosts={drHosts} |
|
fileUuid={fileUuid} |
|
formik={formik} |
|
key={fileUuid} |
|
/> |
|
); |
|
}), |
|
[anvils, drHosts, formik], |
|
); |
|
|
|
return ( |
|
<FlexBox> |
|
<MessageBox> |
|
Uploaded files will be listed automatically, but it may take a while for |
|
larger files to finish uploading and appear on the list. |
|
</MessageBox> |
|
{uploads ? ( |
|
<> |
|
<MessageBox> |
|
This dialog can be closed after all uploads complete. Closing before |
|
completion will stop the upload. |
|
</MessageBox> |
|
<UploadFileProgress uploads={uploads} /> |
|
</> |
|
) : ( |
|
<FlexBox |
|
component="form" |
|
onSubmit={(event) => { |
|
event.preventDefault(); |
|
|
|
formik.submitForm(); |
|
}} |
|
> |
|
<input |
|
id="files" |
|
multiple |
|
name="files" |
|
onChange={handleSelectFiles} |
|
ref={filePickerRef} |
|
style={{ display: 'none' }} |
|
type="file" |
|
/> |
|
<ContainedButton |
|
onClick={() => { |
|
filePickerRef.current?.click(); |
|
}} |
|
> |
|
Browse |
|
</ContainedButton> |
|
{fileInputs} |
|
<MessageGroup count={1} messages={formikErrors} /> |
|
<ActionGroup |
|
actions={[ |
|
{ |
|
background: 'blue', |
|
children: 'Add', |
|
disabled: disableProceed, |
|
type: 'submit', |
|
}, |
|
]} |
|
/> |
|
</FlexBox> |
|
)} |
|
</FlexBox> |
|
); |
|
}; |
|
|
|
export default AddFileForm;
|
|
|