diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 0be2d09e80..b12246e2bd 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -35,6 +35,8 @@ "global.import": "Import", "global.import.fromFile": "From a file", "global.import.fromDatabase": "From database", + "global.import.success": "Import successful", + "global.import.error": "Import failed", "global.launch": "Launch", "global.jobs": "Jobs", "global.unknown": "Unknown", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index c860706f7a..c23f0776f6 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -35,6 +35,8 @@ "global.import": "Importer", "global.import.fromFile": "Depuis un fichier", "global.import.fromDatabase": "Depuis la base de donnée", + "global.import.success": "Importation réussie", + "global.import.error": "Échec de l'importation", "global.launch": "Lancer", "global.jobs": "Tâches", "global.unknown": "Inconnu", diff --git a/webapp/src/components/common/buttons/UploadFileButton.tsx b/webapp/src/components/common/buttons/UploadFileButton.tsx new file mode 100644 index 0000000000..a156825136 --- /dev/null +++ b/webapp/src/components/common/buttons/UploadFileButton.tsx @@ -0,0 +1,112 @@ +import { LoadingButton } from "@mui/lab"; +import FileDownloadIcon from "@mui/icons-material/FileDownload"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { toError } from "../../../utils/fnUtils"; +import { Accept, useDropzone } from "react-dropzone"; +import { StudyMetadata } from "../../../common/types"; +import { useSnackbar } from "notistack"; +import { importFile } from "../../../services/api/studies/raw"; + +type ValidateResult = boolean | null | undefined; +type Validate = (file: File) => ValidateResult | Promise; + +export interface UploadFileButtonProps { + studyId: StudyMetadata["id"]; + path: string | ((file: File) => string); + children?: React.ReactNode; + accept?: Accept; + disabled?: boolean; + onUploadSuccessful?: (file: File) => void; + validate?: Validate; +} + +function UploadFileButton(props: UploadFileButtonProps) { + const { t } = useTranslation(); + const { + studyId, + path, + accept, + disabled, + onUploadSuccessful, + children: label = t("global.import"), + } = props; + + const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); + const { enqueueSnackbar } = useSnackbar(); + const [isUploading, setIsUploading] = useState(false); + const { open } = useDropzone({ onDropAccepted: handleDropAccepted, accept }); + + // Prevent the user from accidentally leaving the page while uploading + useEffect(() => { + if (isUploading) { + const listener = (e: BeforeUnloadEvent) => { + // eslint-disable-next-line no-param-reassign + e.returnValue = t("global.import"); + }; + + window.addEventListener("beforeunload", listener); + + return () => { + window.removeEventListener("beforeunload", listener); + }; + } + }, [isUploading, t]); + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + async function handleDropAccepted(acceptedFiles: File[]) { + setIsUploading(true); + + const fileToUpload = acceptedFiles[0]; + + try { + const isValid = (await props.validate?.(fileToUpload)) ?? true; + + if (!isValid) { + return; + } + + const filePath = typeof path === "function" ? path(fileToUpload) : path; + + await importFile({ + studyId, + path: filePath, + file: fileToUpload, + createMissing: true, + }); + + enqueueSnackbar(t("global.import.success"), { variant: "success" }); + } catch (err) { + enqueueErrorSnackbar(t("global.import.error"), toError(err)); + return; + } finally { + setIsUploading(false); + } + + onUploadSuccessful?.(fileToUpload); + } + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + } + loadingPosition="start" + loading={isUploading} + disabled={disabled} + > + {label} + + ); +} + +export default UploadFileButton;