From 5c02f130566e2c8db1f2fe6baba07b1e78b00a3a Mon Sep 17 00:00:00 2001 From: nidhigarg-bmw <101316912+nidhigarg-bmw@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:21:29 +0530 Subject: [PATCH] fix(error handling): add all api error message from backend (#105) --- CHANGELOG.md | 3 + src/components/Snackbar/index.tsx | 176 ++++++++++++++++++++++++ src/components/cax-companyData.tsx | 28 +++- src/components/cax-companyRole.tsx | 43 +++--- src/components/cax-registration.tsx | 7 +- src/components/cax-responsibilities.tsx | 33 +++-- src/components/dragdrop.tsx | 31 +++-- src/components/landing.tsx | 5 +- src/components/verifyRegistration.tsx | 26 ++-- src/locales/de/translations.json | 3 +- src/locales/en/translations.json | 3 +- 11 files changed, 292 insertions(+), 66 deletions(-) create mode 100644 src/components/Snackbar/index.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5330fe43..713d2f8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Unreleased +- Add error handling of api errors + ## 1.6.0-RC2 ### Bugfix diff --git a/src/components/Snackbar/index.tsx b/src/components/Snackbar/index.tsx new file mode 100644 index 00000000..c9a899d8 --- /dev/null +++ b/src/components/Snackbar/index.tsx @@ -0,0 +1,176 @@ +/******************************************************************************** + * Copyright (c) 2021, 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +import { Box, IconButton, Slide } from '@mui/material' +import { Close } from '@mui/icons-material' +import { type SlideProps } from '@mui/material/Slide/Slide' +import { useCallback, useEffect, useRef, useState } from 'react' +import Snackbar from '@mui/material/Snackbar' +import CheckIcon from '@mui/icons-material/Check' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' + +const AUTO_CLOSE_DELAY_MS = 3000 + +const SlideTransition = (props: SlideProps) => ( + +) + +export enum SeverityType { + SUCCESS = 'success', + ERROR = 'error', +} + +export interface PageSnackbarProps { + severity?: SeverityType + open: boolean + onCloseNotification?: () => void + title?: string | JSX.Element + description?: string | JSX.Element + showIcon?: boolean + autoClose?: boolean +} + +export const PageSnackbar = ({ + severity = SeverityType.SUCCESS, + onCloseNotification, + open, + autoClose, + title, + description, + showIcon = true, + ...props +}: PageSnackbarProps) => { + const [isOpen, setIsOpen] = useState(open) + + const autoCloseTimeoutRef = useRef>() + + useEffect(() => { + setIsOpen(open) + }, [open]) + + const cancelAutoClose = useCallback(() => { + clearTimeout(autoCloseTimeoutRef.current) + }, []) + + const doClose = useCallback(() => { + cancelAutoClose() + setIsOpen(false) + + onCloseNotification?.() + }, [cancelAutoClose, onCloseNotification]) + + const handleAutoClose = useCallback(() => { + cancelAutoClose() + + if (autoClose) { + autoCloseTimeoutRef.current = setTimeout(doClose, AUTO_CLOSE_DELAY_MS) + } + }, [autoClose, cancelAutoClose, doClose]) + + useEffect(handleAutoClose, [autoClose, handleAutoClose]) + + const renderIcon = () => { + switch (severity) { + case 'success': + return + case 'error': + return + } + } + + return ( + + {showIcon && ( + + {renderIcon()} + + )} + + {title && ( + + {title} + + )} + {description} + + + + + + + + } + sx={{ + '.MuiSnackbarContent-root': { + width: '390px', + typography: 'body3', + backgroundColor: 'common.white', + color: 'text.primary', + borderWidth: '1px', + borderStyle: 'solid', + borderColor: 'action.disabledBackground', + borderRadius: '8px', + boxShadow: '0px 10px 20px 0px rgba(80, 80, 80, 0.3)', + }, + '.MuiSnackbarContent-message': { + width: '100%', + }, + }} + /> + ) +} + + +export const Notify = ({ message }: { message: string }) => { + return ( + + ) +} \ No newline at end of file diff --git a/src/components/cax-companyData.tsx b/src/components/cax-companyData.tsx index f710384e..8e386a90 100644 --- a/src/components/cax-companyData.tsx +++ b/src/components/cax-companyData.tsx @@ -26,7 +26,6 @@ import { useTranslation } from 'react-i18next' import { useEffect, useState } from 'react' import { FooterButton } from './footerButton' import { useDispatch, useSelector } from 'react-redux' -import { toast } from 'react-toastify' import { isBPN, isCity, isStreet } from '../types/Patterns' import { useFetchApplicationsQuery, @@ -40,6 +39,7 @@ import { addCurrentStep, getCurrentStep, } from '../state/features/user/userApiSlice' +import { Notify } from './Snackbar' const initialErrors = { legalEntity: '', @@ -83,14 +83,20 @@ export const CompanyDataCax = () => { const [changedCountryValue, setChangedCountryValue] = useState(false) const [errors, setErrors] = useState(initialErrors) - const { data: companyDetails } = + const [submitError, setSubmitError] = useState(false) + const [identifierError, setIdentifierError] = useState(false) + + const { data: companyDetails, error: companyDataError } = useFetchCompanyDetailsWithAddressQuery(applicationId) const [addCompanyDetailsWithAddress, { error: saveError, isLoading }] = useAddCompanyDetailsWithAddressMutation() useEffect(() => { + setSubmitError(false) nextClicked && !isLoading && ( - saveError ? toast.error(t('registrationStepOne.submitError')) : dispatch(addCurrentStep(currentActiveStep + 1)) + saveError ? + setSubmitError(true) : + dispatch(addCurrentStep(currentActiveStep + 1)) ) }, [nextClicked, isLoading, saveError, currentActiveStep]) @@ -101,12 +107,13 @@ export const CompanyDataCax = () => { } = useFetchUniqueIdentifierQuery(country) useEffect(() => { + setIdentifierError(false) setIdentifierDetails(error ? [] : identifierData) if (identifierData?.length > 0) { setShowIdentifiers(!error) } if (country && country.length === 2 && error) - toast.error(t('registrationStepOne.identifierError')) + setIdentifierError(true) }, [identifierData, country, error]) useEffect(() => { @@ -320,6 +327,18 @@ export const CompanyDataCax = () => { setNextClicked(true) } + const renderSnackbar = () => { + let message = t('registration.apiError') + if(identifierError){ + message = t('registrationStepOne.identifierError') + }else if(submitError){ + message = t('registrationStepOne.submitError') + } + return ( + + ) + } + return ( <>
@@ -589,6 +608,7 @@ export const CompanyDataCax = () => { )}
+ {(companyDataError || submitError || identifierError) && renderSnackbar()} backClick()} diff --git a/src/components/cax-companyRole.tsx b/src/components/cax-companyRole.tsx index 83b7a59b..91d5e329 100644 --- a/src/components/cax-companyRole.tsx +++ b/src/components/cax-companyRole.tsx @@ -22,14 +22,13 @@ import { useState, useEffect } from 'react' import { Row } from 'react-bootstrap' import 'react-datepicker/dist/react-datepicker.css' import { useTranslation } from 'react-i18next' -import { toast } from 'react-toastify' import { FooterButton } from './footerButton' import { useDispatch, useSelector } from 'react-redux' import { companyRole } from '../state/features/applicationCompanyRole/types' import { download } from '../helpers/utils' import UserService from '../services/UserService' import { getApiBase } from '../services/EnvironmentService' -import '../styles/newApp.css' +import { Notify } from './Snackbar' import { useFetchAgreementConsentsQuery, useFetchAgreementDataQuery, @@ -40,6 +39,7 @@ import { addCurrentStep, getCurrentStep, } from '../state/features/user/userApiSlice' +import '../styles/newApp.css' export const CompanyRoleCax = () => { const { t, i18n } = useTranslation() @@ -48,6 +48,7 @@ export const CompanyRoleCax = () => { const currentActiveStep = useSelector(getCurrentStep) const [companyRoleChecked, setCompanyRoleChecked] = useState({}) const [agreementChecked, setAgreementChecked] = useState({}) + const [submitError, setSubmitError] = useState(false) const { data: status } = useFetchApplicationsQuery() @@ -56,22 +57,14 @@ export const CompanyRoleCax = () => { const { data: allConsentData, - error: allConsentError, - isLoading: allConsentLoading, + error: allConsentError } = useFetchAgreementDataQuery() const { data: consentData, error: consentError, - isLoading: consentLoading, } = useFetchAgreementConsentsQuery(applicationId) const [updateAgreementConsents] = useUpdateAgreementConsentsMutation() - if ( - (allConsentLoading && allConsentError) || - (consentLoading && consentError) - ) - toast.error('') - useEffect(() => { updateSelectedRolesAndAgreement() }, [consentData]) @@ -208,13 +201,13 @@ export const CompanyRoleCax = () => { ) const agreements = Object.keys(agreementChecked) - .filter(agreementId => agreementChecked[agreementId]) - .map((agreementId) => { - return { - agreementId: agreementId, - consentStatus: 'ACTIVE' - } - }) + .filter(agreementId => agreementChecked[agreementId]) + .map((agreementId) => { + return { + agreementId: agreementId, + consentStatus: 'ACTIVE' + } + }) const data = { companyRoles: companyRoles, @@ -228,10 +221,16 @@ export const CompanyRoleCax = () => { }) .catch((errors) => { console.log('errors', errors) - toast.error(t('companyRole.submitError')) + setSubmitError(true) }) } + const renderSnackbar = (message: string) => { + return ( + + ) + } + return ( <>
@@ -270,6 +269,12 @@ export const CompanyRoleCax = () => { ))}
+ { + (consentError || allConsentError) && renderSnackbar(t('registration.apiError')) + } + { + submitError && renderSnackbar(t('companyRole.submitError')) + } { const { t } = useTranslation() const currentActiveStep = useSelector(getCurrentStep) - const { data: status, error: statusError } = useFetchApplicationsQuery() + const { data: status } = useFetchApplicationsQuery() const [updateInvitation] = useUpdateInvitationMutation() - if (statusError) { - toast.error(t('registration.statusApplicationError')) - } - useEffect(() => { if (status.length <= 0) { updateInvitation() diff --git a/src/components/cax-responsibilities.tsx b/src/components/cax-responsibilities.tsx index 3cda689b..e4c6f123 100644 --- a/src/components/cax-responsibilities.tsx +++ b/src/components/cax-responsibilities.tsx @@ -23,7 +23,6 @@ import 'react-datepicker/dist/react-datepicker.css' import { AiOutlineUser } from 'react-icons/ai' import Button from './button' import { AiOutlineExclamationCircle } from 'react-icons/ai' -import { ToastContainer, toast } from 'react-toastify' import { useDispatch, useSelector } from 'react-redux' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -39,6 +38,7 @@ import { useFetchRolesCompositeQuery, useUpdateInviteNewUserMutation, } from '../state/features/applicationInviteUser/applicationInviteUserApiSlice' +import { Notify, SeverityType } from './Snackbar' export const ResponsibilitiesCax = () => { const { t } = useTranslation() @@ -55,21 +55,20 @@ export const ResponsibilitiesCax = () => { role: '', personalNote: '', }) - const [loading, setLoading] = useState() - const dispatch = useDispatch() const currentActiveStep = useSelector(getCurrentStep) - const { data: status, error: statusError } = useFetchApplicationsQuery() + const [loading, setLoading] = useState() + const [invitedResponse, setInvitedResponse] = useState({severity: SeverityType.ERROR, message: ''}) + + const { data: status } = useFetchApplicationsQuery() const obj = status[status.length - 1] //.find(o => o['applicationStatus'] === CREATED); const applicationId = obj['applicationId'] - - if (statusError) toast.error(toast.error(t('registration.statusApplicationError'))) const [updateInviteNewUser] = useUpdateInviteNewUserMutation() - const { data: rolesComposite } = useFetchRolesCompositeQuery() - const { data: invitedUsers, refetch } = + const { data: rolesComposite, error: rolesError } = useFetchRolesCompositeQuery() + const { data: invitedUsers, error: invitedUsersError, refetch } = useFetchInvitedUsersQuery(applicationId) useEffect(() => { @@ -91,6 +90,7 @@ export const ResponsibilitiesCax = () => { /^[a-zA-Z][a-zA-Z0-9 !#'$@&%()*+\r\n,\-_./:;=<>?[\]\\^]{0,255}$/.test(note) const handleSendInvite = () => { + setInvitedResponse({severity: SeverityType.ERROR, message: ''}) if (email && validateEmail(email)) { setLoading(true) const user = { @@ -108,11 +108,11 @@ export const ResponsibilitiesCax = () => { setEmail('') setMessage('') refetch() - toast.success(t('Responsibility.sendInviteSuccessMsg')) + setInvitedResponse({severity: SeverityType.SUCCESS, message: t('Responsibility.sendInviteSuccessMsg')}) setLoading(false) }) .catch((errors: any) => { - toast.error(errors.data.errors.unknown[0]) + setInvitedResponse({severity: SeverityType.ERROR, message: errors.data.errors.unknown[0]}) setLoading(false) }) } @@ -154,6 +154,14 @@ export const ResponsibilitiesCax = () => { dispatch(addCurrentStep(currentActiveStep + 1)) } + const renderSnackbar = () => { + let message = t('registration.apiError') + if(invitedResponse.message) message = invitedResponse.message + return ( + + ) + } + return ( <>
@@ -228,7 +236,6 @@ export const ResponsibilitiesCax = () => { loading={loading} />
- {invitedUsers?.length > 0 && invitedUsers && ( @@ -270,6 +277,10 @@ export const ResponsibilitiesCax = () => { )} + { + (rolesError || invitedUsersError || invitedResponse.message) && + renderSnackbar() + } { switch (status) { @@ -76,21 +76,20 @@ export const DragDrop = () => { const { t } = useTranslation() const dispatch = useDispatch() - const { data: status, error: statusError } = useFetchApplicationsQuery() + const { data: status } = useFetchApplicationsQuery() const obj = status[status.length - 1] const applicationId = obj['applicationId'] const [fileError, setFileError] = useState('') + const [deleteDocResponse, setDeleteDocResponse] = useState({severity: SeverityType.ERROR, message: ''}) const currentActiveStep = useSelector(getCurrentStep) - const { data: documents } = useFetchDocumentsQuery(applicationId) + const { data: documents, error: documentError } = useFetchDocumentsQuery(applicationId) const [fetchDocumentByDocumentId] = useFetchDocumentByDocumentIdMutation() const [updateStatus] = useUpdateStatusMutation() const [updateDocument] = useUpdateDocumentMutation() const [removeDocument] = useRemoveDocumentMutation() - if (statusError) toast.error(toast.error(t('registration.statusApplicationError'))) - const manageFileStatus = async (fileDetails: FileStatus) => { switch (fileDetails.stats) { case 'done': @@ -133,14 +132,15 @@ export const DragDrop = () => { } const deleteDocumentFn = async (documentId) => { + setDeleteDocResponse({severity: SeverityType.ERROR, message: ''}) await removeDocument(documentId) .unwrap() .then(() => { - toast.success(t('documentUpload.deleteSuccess')) + setDeleteDocResponse({severity: SeverityType.SUCCESS, message: t('documentUpload.deleteSuccess')}) }) .catch((errors: any) => { console.log('errors', errors) - toast.error(t('documentUpload.deleteError')) + setDeleteDocResponse({severity: SeverityType.ERROR, message: t('documentUpload.deleteError')}) }) } @@ -148,7 +148,6 @@ export const DragDrop = () => { documentId: string, documentName: string ) => { - //dispatch(fetchDocumentByDocumentId({ documentId, documentName })) try { const response = await fetchDocumentByDocumentId(documentId).unwrap() const fileType = response.headers.get('content-type') @@ -160,6 +159,14 @@ export const DragDrop = () => { } } + const renderSnackbar = () => { + let message = t('registration.apiError') + if(deleteDocResponse.message) message = deleteDocResponse.message + return ( + + ) + } + return ( <>
@@ -213,11 +220,10 @@ export const DragDrop = () => { {document.documentName}
- {`${getStatusText(document.status)} ${ - document.progress && document?.progress !== 100 + {`${getStatusText(document.status)} ${document.progress && document?.progress !== 100 ? document?.progress : '' - }`} + }`}
{ ))}
+ {(documentError || deleteDocResponse.message) && + renderSnackbar() + } { const { t } = useTranslation() const history = useHistory() - const { data: status, error: statusError } = useFetchApplicationsQuery() + const { data: status } = useFetchApplicationsQuery() const [updateInvitation] = useUpdateInvitationMutation() const [updateStatus] = useUpdateStatusMutation() - if (statusError) toast.error(toast.error(t('registration.statusApplicationError'))) - useEffect(() => { updateInvitation().unwrap() }, []) diff --git a/src/components/verifyRegistration.tsx b/src/components/verifyRegistration.tsx index 258814b5..eb38f25f 100644 --- a/src/components/verifyRegistration.tsx +++ b/src/components/verifyRegistration.tsx @@ -25,7 +25,6 @@ import { FooterButton } from './footerButton' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import { useState } from 'react' -import { ToastContainer, toast } from 'react-toastify' import { useFetchApplicationsQuery } from '../state/features/application/applicationApiSlice' import { useFetchDocumentsQuery } from '../state/features/applicationDocuments/applicationDocumentsApiSlice' import { @@ -36,6 +35,7 @@ import { useFetchRegistrationDataQuery, useUpdateRegistrationMutation, } from '../state/features/applicationVerifyRegister/applicationVerifyRegisterApiSlice' +import { Notify } from './Snackbar' export const VerifyRegistration = () => { const { t } = useTranslation() @@ -45,17 +45,16 @@ export const VerifyRegistration = () => { const currentActiveStep = useSelector(getCurrentStep) const [loading, setLoading] = useState(false) + const [submitError, setSubmitError] = useState(false) - const { data: status, error: statusError } = useFetchApplicationsQuery() + const { data: status } = useFetchApplicationsQuery() const obj = status[status.length - 1] const applicationId = obj['applicationId'] - - if (statusError) toast.error(toast.error(t('registration.statusApplicationError'))) - const { data: registrationData } = + const { data: registrationData, error: dataError } = useFetchRegistrationDataQuery(applicationId) - const { data: documents } = useFetchDocumentsQuery(applicationId) + const { data: documents, error: documentsError } = useFetchDocumentsQuery(applicationId) const [updateRegistration] = useUpdateRegistrationMutation() const backClick = () => { @@ -73,7 +72,7 @@ export const VerifyRegistration = () => { .catch((errors: any) => { console.log('errors', errors) setLoading(false) - toast.error(t('verifyRegistration.submitErrorMessage')) + setSubmitError(true) }) } @@ -91,6 +90,14 @@ export const VerifyRegistration = () => { const hasDocuments = () => documents && documents.length > 0 + const renderSnackbar = () => { + let message = t('registration.apiError') + if(submitError) message = t('verifyRegistration.submitErrorMessage') + return ( + + ) + } + return ( <>
@@ -229,9 +236,10 @@ export const VerifyRegistration = () => {
- - + {(dataError || documentsError || submitError) && + renderSnackbar() + }