From 87a60fe5deb97ddba71a81b6c28a3e5d9c6247ae Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Wed, 18 Sep 2024 11:08:57 -0700 Subject: [PATCH] SIMSBIOHUB-614: Fix file upload error handling. (#1366) * Split the project/survey report/non-report attachment dialogs into 2 components. Made 2 new attachment dialogs - one for single items, one for many items. Added error handling to catch file upload errors. * Add missing JSDoc. Fix project/survey regular attachments error display (shows error in the list, not as a popup). * Add JSDoc. Add fallback catch to report upload. Fix error popup not displaying error message in body of dialog for the csv upload dialogs. --------- Co-authored-by: Macgregor Aubertin-Young <108430771+mauberti-bc@users.noreply.github.com> --- .../attachments/FileUploadWithMeta.tsx | 101 ++++-------- .../components/dialog/FileUploadDialog.tsx | 84 ---------- .../dialog/attachments/FileUploadDialog.tsx | 87 ++++++++++ .../FileUploadSingleItemDialog.tsx | 101 ++++++++++++ .../attachments/FileUploadWithMetaDialog.tsx | 149 ------------------ .../attachments/ReportFileUploadDialog.tsx | 115 ++++++++++++++ app/src/components/file-upload/FileUpload.tsx | 2 +- app/src/constants/i18n.ts | 20 ++- .../projects/view/ProjectAttachments.tsx | 89 +++++------ .../list/components/AnimalListToolbar.tsx | 20 +-- .../ImportObservationsButton.tsx | 18 +-- .../components/ImportObservationsButton.tsx | 18 +-- .../table/TelemetryTableContainer.tsx | 80 ++++++---- .../surveys/view/SurveyAttachments.tsx | 102 ++++++------ 14 files changed, 530 insertions(+), 456 deletions(-) delete mode 100644 app/src/components/dialog/FileUploadDialog.tsx create mode 100644 app/src/components/dialog/attachments/FileUploadDialog.tsx create mode 100644 app/src/components/dialog/attachments/FileUploadSingleItemDialog.tsx delete mode 100644 app/src/components/dialog/attachments/FileUploadWithMetaDialog.tsx create mode 100644 app/src/components/dialog/attachments/ReportFileUploadDialog.tsx diff --git a/app/src/components/attachments/FileUploadWithMeta.tsx b/app/src/components/attachments/FileUploadWithMeta.tsx index 7d7574a94c..380be1950c 100644 --- a/app/src/components/attachments/FileUploadWithMeta.tsx +++ b/app/src/components/attachments/FileUploadWithMeta.tsx @@ -1,84 +1,53 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import ReportMetaForm, { IReportMetaForm } from 'components/attachments/ReportMetaForm'; -import FileUpload, { IReplaceHandler } from 'components/file-upload/FileUpload'; -import { - IFileHandler, - IOnUploadSuccess, - IUploadHandler, - UploadFileStatus -} from 'components/file-upload/FileUploadItem'; -import { AttachmentType, AttachmentTypeFileExtensions } from 'constants/attachments'; +import { UploadFileStatus } from 'components/file-upload/FileUploadItem'; +import { FileUploadSingleItem } from 'components/file-upload/FileUploadSingleItem'; +import { AttachmentTypeFileExtensions } from 'constants/attachments'; import { useFormikContext } from 'formik'; -import React from 'react'; -export interface IFileUploadWithMetaProps { - attachmentType: AttachmentType.REPORT | AttachmentType.KEYX | AttachmentType.CFG | AttachmentType.OTHER; - uploadHandler: IUploadHandler; - fileHandler?: IFileHandler; - onSuccess?: IOnUploadSuccess; -} +/** + * File upload with meta form. Used to upload a report with accompanying meta data. + * + * @return {*} + */ +export const FileUploadWithMeta = () => { + const { handleSubmit, setFieldValue, setFieldError, values, errors } = useFormikContext(); -export const FileUploadWithMeta: React.FC = (props) => { - const { handleSubmit, setFieldValue, errors } = useFormikContext(); - - const fileHandler: IFileHandler = (file) => { + const onFile = (file: File | null) => { setFieldValue('attachmentFile', file); - - props.fileHandler?.(file); + setFieldError('attachmentFile', ''); }; - const replaceHandler: IReplaceHandler = () => { - setFieldValue('attachmentFile', null); + const onError = (error: string) => { + setFieldError('attachmentFile', error); }; return (
- {props.attachmentType === AttachmentType.REPORT && ( - - - - )} - {props.attachmentType === AttachmentType.REPORT && ( - - - Attach File - - - {errors?.attachmentFile && ( - - {/* TODO is errors.attachmentFile correct here? (added `as string` to appease compile warning) */} - {errors.attachmentFile as string} - - )} - - )} - {props.attachmentType === AttachmentType.KEYX && ( - + + + + + Attach File + + - )} - {props.attachmentType === AttachmentType.OTHER && ( - - )} + {errors?.attachmentFile && ( + + {/* TODO is errors.attachmentFile correct here? (added `as string` to appease compile warning) */} + {errors.attachmentFile as string} + + )} + ); }; diff --git a/app/src/components/dialog/FileUploadDialog.tsx b/app/src/components/dialog/FileUploadDialog.tsx deleted file mode 100644 index 5490df8129..0000000000 --- a/app/src/components/dialog/FileUploadDialog.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { LoadingButton } from '@mui/lab'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import useTheme from '@mui/material/styles/useTheme'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import FileUpload, { IFileUploadProps } from 'components/file-upload/FileUpload'; -import { IFileHandler, ISubtextProps, UploadFileStatus } from 'components/file-upload/FileUploadItem'; -import { useState } from 'react'; -import { getFormattedFileSize } from 'utils/Utils'; -import { IComponentDialogProps } from './ComponentDialog'; - -interface IFileUploadDialogProps extends IComponentDialogProps { - uploadButtonLabel?: string; - onUpload: (file: File) => Promise; - FileUploadProps: Partial; -} - -const SubtextComponent = (props: ISubtextProps) => ( - <>{props.status === UploadFileStatus.STAGED ? getFormattedFileSize(props.file.size) : props.error ?? props.status} -); - -const FileUploadDialog = (props: IFileUploadDialogProps) => { - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const [currentFile, setCurrentFile] = useState(null); - const [isUploading, setIsUploading] = useState(false); - - const isDisabled = !currentFile; - - const fileHandler: IFileHandler = (file: File | null) => { - setCurrentFile(file); - }; - - const handleUpload = () => { - if (!currentFile) { - return; - } - - setIsUploading(true); - props.onUpload(currentFile).finally(() => setIsUploading(false)); - }; - - return ( - - {props.dialogTitle} - - {props.children} - - - - handleUpload()} - color="primary" - variant="contained" - autoFocus> - {props.uploadButtonLabel ? props.uploadButtonLabel : 'Import'} - - - - - ); -}; - -export default FileUploadDialog; diff --git a/app/src/components/dialog/attachments/FileUploadDialog.tsx b/app/src/components/dialog/attachments/FileUploadDialog.tsx new file mode 100644 index 0000000000..a6cfb3dca0 --- /dev/null +++ b/app/src/components/dialog/attachments/FileUploadDialog.tsx @@ -0,0 +1,87 @@ +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import useTheme from '@mui/material/styles/useTheme'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { IDropZoneConfigProps } from 'components/file-upload/DropZone'; +import FileUpload from 'components/file-upload/FileUpload'; +import { IUploadHandler } from 'components/file-upload/FileUploadItem'; +import { IComponentDialogProps } from '../ComponentDialog'; + +interface IFileUploadDialogProps extends IComponentDialogProps { + /** + * Set to `true` to open the dialog, `false` to close the dialog. + * + * @type {boolean} + * @memberof IFileUploadDialogProps + */ + open: boolean; + /** + * The title of the dialog. + * + * @type {string} + * @memberof IFileUploadDialogProps + */ + dialogTitle: string; + /** + * Callback fired when a file is added. + * + * @memberof IReportFileUploadDialogProps + */ + uploadHandler: IUploadHandler; + /** + * Callback fired when the dialog is closed. + * + * This function does not need to handle any errors, as the `FileUpload` component handles errors internally. + * + * @memberof IFileUploadDialogProps + */ + onClose: () => void; + /** + * Drop zone configuration properties. + * + * @type {IDropZoneConfigProps} + * @memberof IFileUploadDialogProps + */ + dropZoneProps?: IDropZoneConfigProps; +} + +/** + * Wraps the standard `FileUpload` component in a dialog. + * + * The wrapped `FileUpload` component allows for drag-and-drop file uploads of any number of files with any file type. + * + * @param {IFileUploadDialogProps} props + * @return {*} + */ +export const FileUploadDialog = (props: IFileUploadDialogProps) => { + const { open, dialogTitle, uploadHandler, onClose, dropZoneProps } = props; + + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + if (!open) { + return null; + } + + return ( + + {dialogTitle} + + + + + + + + ); +}; diff --git a/app/src/components/dialog/attachments/FileUploadSingleItemDialog.tsx b/app/src/components/dialog/attachments/FileUploadSingleItemDialog.tsx new file mode 100644 index 0000000000..4dfd48b009 --- /dev/null +++ b/app/src/components/dialog/attachments/FileUploadSingleItemDialog.tsx @@ -0,0 +1,101 @@ +import { LoadingButton } from '@mui/lab'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import useTheme from '@mui/material/styles/useTheme'; +import Typography from '@mui/material/Typography'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { IDropZoneConfigProps } from 'components/file-upload/DropZone'; +import { UploadFileStatus } from 'components/file-upload/FileUploadItem'; +import { FileUploadSingleItem } from 'components/file-upload/FileUploadSingleItem'; +import { useEffect, useState } from 'react'; + +interface IFileUploadSingleItemDialog { + open: boolean; + dialogTitle: string; + uploadButtonLabel: string; + onUpload: (file: File) => Promise; + onClose?: () => void; + dropZoneProps: Pick; +} + +/** + * + * + * @param {IFileUploadSingleItemDialog} props + * @return {*} + */ +export const FileUploadSingleItemDialog = (props: IFileUploadSingleItemDialog) => { + const { open, dialogTitle, uploadButtonLabel, onUpload, onClose, dropZoneProps } = props; + + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + const [currentFile, setCurrentFile] = useState(null); + const [status, setStatus] = useState(UploadFileStatus.STAGED); + const [error, setError] = useState(''); + const [isUploading, setIsUploading] = useState(false); + + const isDisabled = !currentFile; + + const handleUpload = () => { + if (!currentFile) { + return; + } + + setIsUploading(true); + onUpload(currentFile).finally(() => setIsUploading(false)); + }; + + useEffect(() => { + setCurrentFile(null); + }, [open]); + + if (!open) { + return null; + } + + return ( + + {dialogTitle} + + setStatus(status)} + onFile={(file) => { + setCurrentFile(file); + setError(''); + }} + onError={(error) => setError(error)} + onCancel={() => {}} + DropZoneProps={dropZoneProps} + /> + + {error} + + + + handleUpload()} + color="primary" + variant="contained" + autoFocus> + {uploadButtonLabel} + + + + + ); +}; diff --git a/app/src/components/dialog/attachments/FileUploadWithMetaDialog.tsx b/app/src/components/dialog/attachments/FileUploadWithMetaDialog.tsx deleted file mode 100644 index c95f8c8a21..0000000000 --- a/app/src/components/dialog/attachments/FileUploadWithMetaDialog.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { LoadingButton } from '@mui/lab'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import useTheme from '@mui/material/styles/useTheme'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import FileUploadWithMeta from 'components/attachments/FileUploadWithMeta'; -import { IFileHandler, IUploadHandler } from 'components/file-upload/FileUploadItem'; -import { AttachmentType } from 'constants/attachments'; -import { Formik, FormikProps } from 'formik'; -import React, { useRef, useState } from 'react'; -import { - IReportMetaForm, - ReportMetaFormInitialValues, - ReportMetaFormYupSchema -} from '../../attachments/ReportMetaForm'; - -/** - * - * - * @export - * @interface IFileUploadWithMetaDialogProps - */ -export interface IFileUploadWithMetaDialogProps { - /** - * The dialog window title text. - * - * @type {string} - * @memberof IFileUploadWithMetaDialogProps - */ - dialogTitle: string; - /** - * The type of attachment. - * - * @type {('Report' | 'KeyX' | 'Cfg' | 'Other')} - * @memberof IFileUploadWithMetaDialogProps - */ - attachmentType: AttachmentType.REPORT | AttachmentType.KEYX | AttachmentType.CFG | AttachmentType.OTHER; - /** - * Set to `true` to open the dialog, `false` to close the dialog. - * - * @type {boolean} - * @memberof IFileUploadWithMetaDialogProps - */ - open: boolean; - /** - * Callback fired if the dialog is finished. - * - * @memberof IFileUploadWithMetaDialogProps - */ - onFinish: (fileMeta: IReportMetaForm) => Promise; - /** - * Callback fired if the dialog is closed. - * - * @memberof IFileUploadWithMetaDialogProps - */ - onClose: () => void; - /** - * Callback fired if an upload request is initiated. - * - * @memberof IFileUploadWithMetaDialogProps - */ - uploadHandler: IUploadHandler; - /** - * Callback fired if a file is added (via browser or drag/drop). - * - * @type {IFileHandler} - * @memberof IFileUploadWithMetaDialogProps - */ - fileHandler?: IFileHandler; -} - -/** - * A dialog to wrap any component(s) that need to be displayed as a modal. - * - * Any component(s) passed in `props.children` will be rendered as the content of the dialog. - * - * @param {*} props - * @return {*} - */ -const FileUploadWithMetaDialog: React.FC = (props) => { - const theme = useTheme(); - - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const formikRef = useRef>(null); - - const [isFinishing, setIsFinishing] = useState(false); - - if (!props.open) { - return <>; - } - - return ( - - { - setIsFinishing(true); - props.onFinish(values).finally(() => { - setIsFinishing(false); - props.onClose(); - }); - }}> - {(formikProps) => ( - <> - {props.dialogTitle} - - - - - {props.attachmentType === AttachmentType.REPORT && ( - - Finish - - )} - {(props.attachmentType === AttachmentType.REPORT && ( - - )) || ( - - )} - - - )} - - - ); -}; - -export default FileUploadWithMetaDialog; diff --git a/app/src/components/dialog/attachments/ReportFileUploadDialog.tsx b/app/src/components/dialog/attachments/ReportFileUploadDialog.tsx new file mode 100644 index 0000000000..747cd2887d --- /dev/null +++ b/app/src/components/dialog/attachments/ReportFileUploadDialog.tsx @@ -0,0 +1,115 @@ +import { LoadingButton } from '@mui/lab'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import useTheme from '@mui/material/styles/useTheme'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import FileUploadWithMeta from 'components/attachments/FileUploadWithMeta'; +import { Formik, FormikProps } from 'formik'; +import React, { useRef, useState } from 'react'; +import { + IReportMetaForm, + ReportMetaFormInitialValues, + ReportMetaFormYupSchema +} from '../../attachments/ReportMetaForm'; + +/** + * + * + * @export + * @interface IReportFileUploadDialogProps + */ +export interface IReportFileUploadDialogProps { + /** + * Set to `true` to open the dialog, `false` to close the dialog. + * + * @type {boolean} + * @memberof IReportFileUploadDialogProps + */ + open: boolean; + /** + * Callback fired if the dialog is submitted (user clicks 'Save' or 'Submit', etc). + * + * The function should handle any errors thrown. + * + * @memberof IReportFileUploadDialogProps + */ + onSubmit: (reportMeta: IReportMetaForm) => Promise; + /** + * Callback fired if the dialog is closed. + * + * @memberof IReportFileUploadDialogProps + */ + onClose: () => void; +} + +/** + * Wraps the `FileUploadWithMeta` component in a dialog. + * + * @param {*} props + * @return {*} + */ +export const ReportFileUploadDialog: React.FC = (props) => { + const theme = useTheme(); + + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + const formikRef = useRef>(null); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = (values: IReportMetaForm) => { + setIsSubmitting(true); + + props + .onSubmit(values) + .catch() // Errors should be handled in the onSubmit function, squash errors here to prevent unhandled errors + .finally(() => { + setIsSubmitting(false); + }); + }; + + if (!props.open) { + return <>; + } + + return ( + + + {(formikProps) => ( + <> + Upload Report + + + + + + Save and Exit + + + + + )} + + + ); +}; diff --git a/app/src/components/file-upload/FileUpload.tsx b/app/src/components/file-upload/FileUpload.tsx index 73f34e89eb..50d5507747 100644 --- a/app/src/components/file-upload/FileUpload.tsx +++ b/app/src/components/file-upload/FileUpload.tsx @@ -41,7 +41,7 @@ export interface IFileUploadProps { */ fileHandler?: IFileHandler; /** - * Callback fired when `uploadHandler` runs successfully fora given file. Will run once for each file that is + * Callback fired when `uploadHandler` runs successfully for a given file. Will run once for each file that is * uploaded. * * @type {IOnUploadSuccess} diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index e216532879..068bd3f0fb 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -63,6 +63,20 @@ export const AttachmentsI18N = { 'An error has occurred while attempting to download an attachment, please try again. If the error persists, please contact your system administrator.' }; +export const ReportI18N = { + cancelTitle: 'Cancel Upload', + cancelText: 'Are you sure you want to cancel?', + uploadErrorTitle: 'Error Uploading Report', + uploadErrorText: + 'An error has occurred while attempting to upload the report, please try again. If the error persists, please contact your system administrator.', + deleteErrorTitle: 'Error Deleting Report', + deleteErrorText: + 'An error has occurred while attempting to delete the report, please try again. If the error persists, please contact your system administrator.', + downloadErrorTitle: 'Error Downloading Report', + downloadErrorText: + 'An error has occurred while attempting to download the report, please try again. If the error persists, please contact your system administrator.' +}; + export const AccessRequestI18N = { requestTitle: 'Access Request', requestText: 'Error requesting access', @@ -447,7 +461,11 @@ export const TelemetryTableI18N = { saveRecordsSuccessSnackbarMessage: 'Telemetry updated successfully.', deleteSingleRecordSuccessSnackbarMessage: 'Deleted telemetry record successfully.', deleteMultipleRecordSuccessSnackbarMessage: (count: number) => - `Deleted ${count} telemetry ${p(count, 'record')} successfully.` + `Deleted ${count} telemetry ${p(count, 'record')} successfully.`, + // Animal CSV import strings + importRecordsSuccessSnackbarMessage: 'Telemetry imported successfully.', + importRecordsErrorDialogTitle: 'Error Importing telemetry Records', + importRecordsErrorDialogText: 'An error occurred while importing telemetry records.' }; export const TelemetryDeviceKeyFileI18N = { diff --git a/app/src/features/projects/view/ProjectAttachments.tsx b/app/src/features/projects/view/ProjectAttachments.tsx index 92526dbd85..174e6924d3 100644 --- a/app/src/features/projects/view/ProjectAttachments.tsx +++ b/app/src/features/projects/view/ProjectAttachments.tsx @@ -4,62 +4,56 @@ import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import { IReportMetaForm } from 'components/attachments/ReportMetaForm'; -import FileUploadWithMetaDialog from 'components/dialog/attachments/FileUploadWithMetaDialog'; -import { IUploadHandler } from 'components/file-upload/FileUploadItem'; +import { FileUploadDialog } from 'components/dialog/attachments/FileUploadDialog'; +import { ReportFileUploadDialog } from 'components/dialog/attachments/ReportFileUploadDialog'; import { ProjectRoleGuard } from 'components/security/Guards'; import { H2MenuToolbar } from 'components/toolbar/ActionToolbars'; +import { ReportI18N } from 'constants/i18n'; import { PROJECT_PERMISSION, SYSTEM_ROLE } from 'constants/roles'; import { ProjectContext } from 'contexts/projectContext'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { IUploadAttachmentResponse } from 'interfaces/useProjectApi.interface'; +import { useDialogContext } from 'hooks/useContext'; import { useContext, useEffect, useState } from 'react'; -import { AttachmentType } from '../../../constants/attachments'; import ProjectAttachmentsList from './ProjectAttachmentsList'; /** - * Project attachments content for a project. + * Project attachments component. * * @return {*} */ const ProjectAttachments = () => { const biohubApi = useBiohubApi(); + const dialogContext = useDialogContext(); const projectContext = useContext(ProjectContext); - const [openUploadAttachments, setOpenUploadAttachments] = useState(false); - const [attachmentType, setAttachmentType] = useState( - AttachmentType.OTHER - ); + const [openUploadDialog, setOpenUploadDialog] = useState<'Attachment' | 'Report' | false>(false); - const handleUploadReportClick = () => { - setAttachmentType(AttachmentType.REPORT); - setOpenUploadAttachments(true); - }; + const onSubmitReport = async (fileMeta: IReportMetaForm) => { + try { + await biohubApi.project.uploadProjectReports(projectContext.projectId, fileMeta.attachmentFile, fileMeta); + } catch (error) { + const apiError = error as APIError; - const handleUploadAttachmentClick = () => { - setAttachmentType(AttachmentType.OTHER); - setOpenUploadAttachments(true); + dialogContext.setErrorDialog({ + open: true, + dialogTitle: ReportI18N.uploadErrorTitle, + dialogText: ReportI18N.uploadErrorText, + dialogError: apiError.message, + dialogErrorDetails: apiError.errors, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + } + }); + } }; - const getUploadHandler = (): IUploadHandler => { - return (file, cancelToken, handleFileUploadProgress) => { - return biohubApi.project.uploadProjectAttachments( - projectContext.projectId, - file, - cancelToken, - handleFileUploadProgress - ); - }; - }; - - const getFinishHandler = () => { - return (fileMeta: IReportMetaForm) => { - return biohubApi.project - .uploadProjectReports(projectContext.projectId, fileMeta.attachmentFile, fileMeta) - .finally(() => { - setOpenUploadAttachments(false); - }); - }; + const handleUploadAttachments = async (file: File) => { + return biohubApi.project.uploadProjectAttachments(projectContext.projectId, file); }; useEffect(() => { @@ -68,16 +62,23 @@ const ProjectAttachments = () => { return ( <> - { + projectContext.artifactDataLoader.refresh(projectContext.projectId); + setOpenUploadDialog(false); + }} + /> + + { - setOpenUploadAttachments(false); projectContext.artifactDataLoader.refresh(projectContext.projectId); + setOpenUploadDialog(false); }} - uploadHandler={getUploadHandler()} /> { { menuLabel: 'Upload a Report', menuIcon: , - menuOnClick: handleUploadReportClick + menuOnClick: () => setOpenUploadDialog('Report') }, { menuLabel: 'Upload Attachments', menuIcon: , - menuOnClick: handleUploadAttachmentClick + menuOnClick: () => setOpenUploadDialog('Attachment') } ]} renderButton={(buttonProps) => ( diff --git a/app/src/features/surveys/animals/list/components/AnimalListToolbar.tsx b/app/src/features/surveys/animals/list/components/AnimalListToolbar.tsx index 37d95485bb..5ea6c5cb85 100644 --- a/app/src/features/surveys/animals/list/components/AnimalListToolbar.tsx +++ b/app/src/features/surveys/animals/list/components/AnimalListToolbar.tsx @@ -4,10 +4,10 @@ import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; -import FileUploadDialog from 'components/dialog/FileUploadDialog'; -import { UploadFileStatus } from 'components/file-upload/FileUploadItem'; +import { FileUploadSingleItemDialog } from 'components/dialog/attachments/FileUploadSingleItemDialog'; import { SurveyAnimalsI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { useSurveyContext } from 'hooks/useContext'; import { useContext, useState } from 'react'; @@ -38,11 +38,14 @@ export const AnimalListToolbar = (props: IAnimaListToolbarProps) => { try { await biohubApi.survey.importCrittersFromCsv(file, surveyContext.projectId, surveyContext.surveyId); surveyContext.critterDataLoader.refresh(surveyContext.projectId, surveyContext.surveyId); - } catch (err: any) { + } catch (error) { + const apiError = error as APIError; + dialogContext.setErrorDialog({ dialogTitle: SurveyAnimalsI18N.importRecordsErrorDialogTitle, dialogText: SurveyAnimalsI18N.importRecordsErrorDialogText, - dialogErrorDetails: [err.message], + dialogError: apiError.message, + dialogErrorDetails: apiError.errors, open: true, onClose: () => { dialogContext.setErrorDialog({ open: false }); @@ -58,16 +61,13 @@ export const AnimalListToolbar = (props: IAnimaListToolbarProps) => { return ( <> - setOpenImportDialog(false)} onUpload={handleImportAnimals} uploadButtonLabel="Import" - FileUploadProps={{ - dropZoneProps: { maxNumFiles: 1, acceptedFileExtensions: '.csv' }, - status: UploadFileStatus.STAGED - }} + dropZoneProps={{ acceptedFileExtensions: '.csv' }} /> { dialogContext.setErrorDialog({ open: false }); @@ -120,16 +123,13 @@ export const ImportObservationsButton = (props: IImportObservationsButtonProps) disabled={disabled || false}> Import - setOpen(false)} onUpload={handleImportObservations} uploadButtonLabel="Import" - FileUploadProps={{ - dropZoneProps: { maxNumFiles: 1, acceptedFileExtensions: '.csv' }, - status: UploadFileStatus.STAGED - }} + dropZoneProps={{ acceptedFileExtensions: '.csv' }} /> ); diff --git a/app/src/features/surveys/observations/sampling-sites/components/ImportObservationsButton.tsx b/app/src/features/surveys/observations/sampling-sites/components/ImportObservationsButton.tsx index 4b0a1ef9b5..9efab72aae 100644 --- a/app/src/features/surveys/observations/sampling-sites/components/ImportObservationsButton.tsx +++ b/app/src/features/surveys/observations/sampling-sites/components/ImportObservationsButton.tsx @@ -1,10 +1,10 @@ import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import FileUploadDialog from 'components/dialog/FileUploadDialog'; -import { UploadFileStatus } from 'components/file-upload/FileUploadItem'; +import { FileUploadSingleItemDialog } from 'components/dialog/attachments/FileUploadSingleItemDialog'; import { ObservationsTableI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; import { SurveyContext } from 'contexts/surveyContext'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { useContext, useState } from 'react'; @@ -107,11 +107,14 @@ export const ImportObservationsButton = (props: IImportObservationsButtonProps) }); onSuccess?.(); - } catch (apiError: any) { + } catch (error) { + const apiError = error as APIError; + dialogContext.setErrorDialog({ dialogTitle: ObservationsTableI18N.importRecordsErrorDialogTitle, dialogText: ObservationsTableI18N.importRecordsErrorDialogText, - dialogErrorDetails: [apiError.message], + dialogError: apiError.message, + dialogErrorDetails: apiError.errors, open: true, onClose: () => { dialogContext.setErrorDialog({ open: false }); @@ -138,16 +141,13 @@ export const ImportObservationsButton = (props: IImportObservationsButtonProps) disabled={disabled || false}> Import - setOpen(false)} onUpload={handleImportObservations} uploadButtonLabel="Import" - FileUploadProps={{ - dropZoneProps: { maxNumFiles: 1, acceptedFileExtensions: '.csv' }, - status: UploadFileStatus.STAGED - }} + dropZoneProps={{ acceptedFileExtensions: '.csv' }} /> ); diff --git a/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx b/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx index 1115fbf82e..21e4b8ae61 100644 --- a/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx +++ b/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx @@ -16,13 +16,13 @@ import Stack from '@mui/material/Stack'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import DataGridValidationAlert from 'components/data-grid/DataGridValidationAlert'; -import FileUploadDialog from 'components/dialog/FileUploadDialog'; +import { FileUploadSingleItemDialog } from 'components/dialog/attachments/FileUploadSingleItemDialog'; import YesNoDialog from 'components/dialog/YesNoDialog'; -import { UploadFileStatus } from 'components/file-upload/FileUploadItem'; import { TelemetryTableI18N } from 'constants/i18n'; import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; import { SurveyContext } from 'contexts/surveyContext'; import { TelemetryTable } from 'features/surveys/telemetry/table/TelemetryTable'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { useTelemetryTableContext } from 'hooks/useContext'; import { useContext, useDeferredValue, useState } from 'react'; @@ -57,49 +57,61 @@ export const TelemetryTableContainer = () => { setColumnVisibilityMenuAnchorEl(null); }; - const handleFileImport = async (file: File) => { - biohubApi.telemetry.uploadCsvForImport(surveyContext.projectId, surveyContext.surveyId, file).then((response) => { + const handleImportTelemetry = async (file: File) => { + try { + const uploadResponse = await biohubApi.telemetry.uploadCsvForImport( + surveyContext.projectId, + surveyContext.surveyId, + file + ); + setShowImportDialog(false); + setProcessingRecords(true); - biohubApi.telemetry - .processTelemetryCsvSubmission(response.submission_id) - .then(() => { - showSnackBar({ - snackbarMessage: ( - - Telemetry imported successfully. - - ) - }); - telemetryTableContext.refreshRecords().then(() => { - setProcessingRecords(false); - }); - }) - .catch((error) => { - showSnackBar({ - snackbarMessage: ( - - {error.message} - - ) - }); + + await biohubApi.telemetry.processTelemetryCsvSubmission(uploadResponse.submission_id); + + showSnackBar({ + snackbarMessage: ( + + Telemetry imported successfully. + + ) + }); + + telemetryTableContext.refreshRecords().then(() => { + setProcessingRecords(false); + }); + } catch (error) { + const apiError = error as APIError; + + dialogContext.setErrorDialog({ + dialogTitle: TelemetryTableI18N.importRecordsErrorDialogTitle, + dialogText: TelemetryTableI18N.importRecordsErrorDialogText, + dialogError: apiError.message, + dialogErrorDetails: apiError.errors, + open: true, + onClose: () => { setProcessingRecords(false); - }); - }); + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + setProcessingRecords(false); + dialogContext.setErrorDialog({ open: false }); + } + }); + } }; return ( <> - setShowImportDialog(false)} - onUpload={handleFileImport} + onUpload={handleImportTelemetry} uploadButtonLabel="Import" - FileUploadProps={{ - dropZoneProps: { maxNumFiles: 1, acceptedFileExtensions: '.csv' }, - status: UploadFileStatus.STAGED - }} + dropZoneProps={{ acceptedFileExtensions: '.csv' }} /> { const biohubApi = useBiohubApi(); + const dialogContext = useDialogContext(); const surveyContext = useContext(SurveyContext); const { projectId, surveyId } = surveyContext; - const [openUploadAttachments, setOpenUploadAttachments] = useState(false); - const [attachmentType, setAttachmentType] = useState( - AttachmentType.OTHER - ); - - const handleUploadReportClick = () => { - setAttachmentType(AttachmentType.REPORT); - setOpenUploadAttachments(true); - }; - - const handleUploadAttachmentClick = () => { - setAttachmentType(AttachmentType.OTHER); - setOpenUploadAttachments(true); - }; + const [openUploadDialog, setOpenUploadDialog] = useState<'Attachment' | 'Report' | false>(false); - const getUploadHandler = (): IUploadHandler => { - return (file, cancelToken, handleFileUploadProgress) => { - return biohubApi.survey.uploadSurveyAttachments(projectId, surveyId, file, cancelToken, handleFileUploadProgress); - }; - }; + const onSubmitReport = async (fileMeta: IReportMetaForm) => { + try { + await biohubApi.survey.uploadSurveyReports(projectId, surveyId, fileMeta.attachmentFile, fileMeta); + } catch (error) { + const apiError = error as APIError; - const getFinishHandler = () => { - return async (fileMeta: IReportMetaForm) => { - return biohubApi.survey - .uploadSurveyReports(projectId, surveyId, fileMeta.attachmentFile, fileMeta) - .finally(() => { - setOpenUploadAttachments(false); - }); - }; + dialogContext.setErrorDialog({ + open: true, + dialogTitle: ReportI18N.uploadErrorTitle, + dialogText: ReportI18N.uploadErrorText, + dialogError: apiError.message, + dialogErrorDetails: apiError.errors, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + } + }); + } }; - const getDialogTitle = () => { - switch (attachmentType) { - case AttachmentType.REPORT: - return 'Upload Report'; - case AttachmentType.OTHER: - return 'Upload Attachments'; - default: - return ''; - } + const handleUploadAttachments = async (file: File) => { + return biohubApi.survey.uploadSurveyAttachments(projectId, surveyId, file); }; return ( <> - { - setOpenUploadAttachments(false); surveyContext.artifactDataLoader.refresh(projectId, surveyId); + setOpenUploadDialog(false); }} - uploadHandler={getUploadHandler()} /> + + { + surveyContext.artifactDataLoader.refresh(projectId, surveyId); + setOpenUploadDialog(false); + }} + /> + { { menuLabel: 'Upload a Report', menuIcon: , - menuOnClick: handleUploadReportClick + menuOnClick: () => setOpenUploadDialog('Report') }, { menuLabel: 'Upload Attachments', menuIcon: , - menuOnClick: handleUploadAttachmentClick + menuOnClick: () => setOpenUploadDialog('Attachment') } ]} renderButton={(buttonProps) => ( @@ -104,7 +106,9 @@ const SurveyAttachments = () => { )} /> - + + +