From c507fe76333b4b076b3c7858cccae7439a9c5d65 Mon Sep 17 00:00:00 2001 From: ruzniaievdm Date: Thu, 29 Feb 2024 19:53:06 +0200 Subject: [PATCH 1/3] feat: [AXIMST-538] Add errors handling 4xx, 5xx --- src/certificates/data/selectors.js | 1 + src/certificates/data/slice.js | 5 +- src/certificates/data/thunks.js | 13 ++-- src/certificates/layout/MainLayout.jsx | 7 ++ src/certificates/layout/hooks/useLayout.jsx | 6 +- src/course-unit/CourseUnit.jsx | 23 ++----- src/course-unit/data/selectors.js | 1 + src/course-unit/data/slice.js | 5 +- src/course-unit/data/thunk.js | 15 +++-- src/course-unit/hooks.jsx | 18 ++--- .../SavingErrorNotification.jsx | 67 +++++++++++++++++++ .../saving-error-notification/index.js | 2 + .../saving-error-notification/messages.js | 22 ++++++ .../saving-error-notification/utils.js | 26 +++++++ src/group-configurations/data/selectors.js | 1 + src/group-configurations/data/slice.js | 5 +- src/group-configurations/data/thunk.js | 32 +++++---- src/group-configurations/hooks.jsx | 3 + src/group-configurations/index.jsx | 21 +++--- src/i18n/messages/ar.json | 6 +- src/i18n/messages/de.json | 6 +- src/i18n/messages/de_DE.json | 6 +- src/i18n/messages/es_419.json | 6 +- src/i18n/messages/fa_IR.json | 6 +- src/i18n/messages/fr.json | 6 +- src/i18n/messages/fr_CA.json | 6 +- src/i18n/messages/hi.json | 6 +- src/i18n/messages/it.json | 6 +- src/i18n/messages/it_IT.json | 6 +- src/i18n/messages/pt.json | 6 +- src/i18n/messages/pt_PT.json | 6 +- src/i18n/messages/ru.json | 6 +- src/i18n/messages/uk.json | 6 +- src/i18n/messages/zh_CN.json | 6 +- src/textbooks/Textbooks.jsx | 12 ++-- src/textbooks/data/selectors.js | 1 + src/textbooks/data/slice.js | 5 +- src/textbooks/data/thunk.js | 7 +- src/textbooks/hooks.jsx | 6 +- 39 files changed, 296 insertions(+), 98 deletions(-) create mode 100644 src/generic/saving-error-notification/SavingErrorNotification.jsx create mode 100644 src/generic/saving-error-notification/index.js create mode 100644 src/generic/saving-error-notification/messages.js create mode 100644 src/generic/saving-error-notification/utils.js diff --git a/src/certificates/data/selectors.js b/src/certificates/data/selectors.js index c269ef3e22..4e2cfdaccc 100644 --- a/src/certificates/data/selectors.js +++ b/src/certificates/data/selectors.js @@ -3,6 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; export const getLoadingStatus = (state) => state.certificates.loadingStatus; export const getSavingStatus = (state) => state.certificates.savingStatus; export const getSavingImageStatus = (state) => state.certificates.savingImageStatus; +export const getErrorMessage = (state) => state.certificates.errorMessage; export const getSendRequestErrors = (state) => state.certificates.sendRequestErrors.developer_message; export const getCertificates = state => state.certificates.certificatesData.certificates; export const getHasCertificateModes = state => state.certificates.certificatesData.hasCertificateModes; diff --git a/src/certificates/data/slice.js b/src/certificates/data/slice.js index 204733c315..a78c48a754 100644 --- a/src/certificates/data/slice.js +++ b/src/certificates/data/slice.js @@ -12,10 +12,13 @@ const slice = createSlice({ loadingStatus: RequestStatus.PENDING, savingStatus: '', savingImageStatus: '', + errorMessage: '', }, reducers: { updateSavingStatus: (state, { payload }) => { - state.savingStatus = payload.status; + const { status, errorMessage } = payload; + state.savingStatus = status; + state.errorMessage = errorMessage; }, updateSavingImageStatus: (state, { payload }) => { state.savingImageStatus = payload.status; diff --git a/src/certificates/data/thunks.js b/src/certificates/data/thunks.js index b0c81e44ba..4c1fca9fd6 100644 --- a/src/certificates/data/thunks.js +++ b/src/certificates/data/thunks.js @@ -3,6 +3,7 @@ import { hideProcessingNotification, showProcessingNotification, } from '../../generic/processing-notification/data/slice'; +import { handleResponseErrors } from '../../generic/saving-error-notification'; import { NOTIFICATION_MESSAGES } from '../../constants'; import { getCertificates, @@ -51,8 +52,7 @@ export function createCourseCertificate(courseId, certificate) { dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); return true; } catch (error) { - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); - return false; + return handleResponseErrors(error, dispatch, updateSavingStatus); } finally { dispatch(hideProcessingNotification()); } @@ -70,8 +70,7 @@ export function updateCourseCertificate(courseId, certificate) { dispatch(updateCertificateSuccess(certificatesValues)); return true; } catch (error) { - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); - return false; + return handleResponseErrors(error, dispatch, updateSavingStatus); } finally { dispatch(hideProcessingNotification()); } @@ -89,8 +88,7 @@ export function deleteCourseCertificate(courseId, certificateId) { dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); return true; } catch (error) { - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); - return false; + return handleResponseErrors(error, dispatch, updateSavingStatus); } finally { dispatch(hideProcessingNotification()); } @@ -111,8 +109,7 @@ export function updateCertificateActiveStatus(courseId, path, activationStatus) dispatch(fetchCertificates(courseId)); return true; } catch (error) { - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); - return false; + return handleResponseErrors(error, dispatch, updateSavingStatus); } finally { dispatch(hideProcessingNotification()); } diff --git a/src/certificates/layout/MainLayout.jsx b/src/certificates/layout/MainLayout.jsx index 7785c9aed4..9494b00388 100644 --- a/src/certificates/layout/MainLayout.jsx +++ b/src/certificates/layout/MainLayout.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import { Container, Layout } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { SavingErrorNotification } from '../../generic/saving-error-notification'; import ProcessingNotification from '../../generic/processing-notification'; import InternetConnectionAlert from '../../generic/internet-connection-alert'; import SubHeader from '../../generic/sub-header/SubHeader'; @@ -14,6 +15,8 @@ const MainLayout = ({ courseId, showHeaderButtons, children }) => { const intl = useIntl(); const { + errorMessage, + savingStatus, isQueryPending, isQueryFailed, isShowProcessingNotification, @@ -54,6 +57,10 @@ const MainLayout = ({ courseId, showHeaderButtons, children }) => { isShow={isShowProcessingNotification} title={processingNotificationTitle} /> + { const savingStatus = useSelector(getSavingStatus); const savingImageStatus = useSelector(getSavingImageStatus); + const errorMessage = useSelector(getErrorMessage); + const { isShow: isShowProcessingNotification, title: processingNotificationTitle, @@ -23,6 +25,8 @@ const useLayout = () => { }, [savingStatus]); return { + errorMessage, + savingStatus, isQueryPending, isQueryFailed, isShowProcessingNotification, diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index 9ba7746111..5278d51f88 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -4,8 +4,8 @@ import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { Container, Layout, Stack } from '@openedx/paragon'; import { useIntl, injectIntl } from '@edx/frontend-platform/i18n'; -import { DraggableList, ErrorAlert } from '@edx/frontend-lib-content-components'; import { Warning as WarningIcon } from '@openedx/paragon/icons'; +import { DraggableList } from '@edx/frontend-lib-content-components'; import { getProcessingNotification } from '../generic/processing-notification/data/selectors'; import SubHeader from '../generic/sub-header/SubHeader'; @@ -13,7 +13,7 @@ import { RequestStatus } from '../data/constants'; import getPageHeadTitle from '../generic/utils'; import AlertMessage from '../generic/alert-message'; import ProcessingNotification from '../generic/processing-notification'; -import InternetConnectionAlert from '../generic/internet-connection-alert'; +import { SavingErrorNotification } from '../generic/saving-error-notification'; import ConnectionErrorAlert from '../generic/ConnectionErrorAlert'; import Loading from '../generic/Loading'; import AddComponent from './add-component/AddComponent'; @@ -34,14 +34,12 @@ const CourseUnit = ({ courseId }) => { isLoading, sequenceId, unitTitle, - isQueryPending, + errorMessage, sequenceStatus, savingStatus, isTitleEditFormOpen, - isErrorAlert, staticFileNotices, currentlyVisibleToStudents, - isInternetConnectionAlertFailed, unitXBlockActions, sharedClipboardData, showPasteXBlock, @@ -49,7 +47,6 @@ const CourseUnit = ({ courseId }) => { handleTitleEditSubmit, headerNavigationsActions, handleTitleEdit, - handleInternetConnectionFailed, handleCreateNewCourseXBlock, handleConfigureSubmit, courseVerticalChildren, @@ -95,9 +92,6 @@ const CourseUnit = ({ courseId }) => { <>
- - {intl.formatMessage(messages.alertFailedGeneric, { actionName: 'save', type: 'changes' })} - { isShow={isShowProcessingNotification} title={processingNotificationTitle} /> - {isQueryPending && ( - - )} + ); diff --git a/src/course-unit/data/selectors.js b/src/course-unit/data/selectors.js index 1d298edbea..d50eeab774 100644 --- a/src/course-unit/data/selectors.js +++ b/src/course-unit/data/selectors.js @@ -5,6 +5,7 @@ export const getCourseUnitData = (state) => state.courseUnit.unit; export const getCanEdit = (state) => state.courseUnit.canEdit; export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices; export const getSavingStatus = (state) => state.courseUnit.savingStatus; +export const getErrorMessage = (state) => state.courseUnit.errorMessage; export const getSequenceStatus = (state) => state.courseUnit.sequenceStatus; export const getSequenceIds = (state) => state.courseUnit.courseSectionVertical.courseSequenceIds; export const getCourseSectionVertical = (state) => state.courseUnit.courseSectionVertical; diff --git a/src/course-unit/data/slice.js b/src/course-unit/data/slice.js index 85e2f08906..5c76e4e41a 100644 --- a/src/course-unit/data/slice.js +++ b/src/course-unit/data/slice.js @@ -7,6 +7,7 @@ const slice = createSlice({ name: 'courseUnit', initialState: { savingStatus: '', + errorMessage: '', isQueryPending: false, isTitleEditFormOpen: false, canEdit: true, @@ -38,7 +39,9 @@ const slice = createSlice({ state.isTitleEditFormOpen = payload; }, updateSavingStatus: (state, { payload }) => { - state.savingStatus = payload.status; + const { status, errorMessage } = payload; + state.savingStatus = status; + state.errorMessage = errorMessage; }, fetchSequenceRequest: (state, { payload }) => { state.sequenceId = payload.sequenceId; diff --git a/src/course-unit/data/thunk.js b/src/course-unit/data/thunk.js index 2c91c6f01d..9e2d61bb69 100644 --- a/src/course-unit/data/thunk.js +++ b/src/course-unit/data/thunk.js @@ -5,6 +5,7 @@ import { hideProcessingNotification, showProcessingNotification, } from '../../generic/processing-notification/data/slice'; +import { handleResponseErrors } from '../../generic/saving-error-notification'; import { RequestStatus } from '../../data/constants'; import { NOTIFICATION_MESSAGES } from '../../constants'; import { updateModel, updateModels } from '../../generic/model-store'; @@ -117,7 +118,7 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) { }); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + handleResponseErrors(error, dispatch, updateSavingStatus); } }; } @@ -142,7 +143,7 @@ export function editCourseUnitVisibilityAndData(itemId, type, isVisible, groupAc }); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + handleResponseErrors(error, dispatch, updateSavingStatus); } }; } @@ -185,7 +186,7 @@ export function createNewCourseXBlock(body, callback, blockId) { } catch (error) { dispatch(hideProcessingNotification()); dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.FAILED })); - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + handleResponseErrors(error, dispatch, updateSavingStatus); } }; } @@ -220,7 +221,7 @@ export function deleteUnitItemQuery(itemId, xblockId) { dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + handleResponseErrors(error, dispatch, updateSavingStatus); } }; } @@ -243,7 +244,7 @@ export function duplicateUnitItemQuery(itemId, xblockId) { dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + handleResponseErrors(error, dispatch, updateSavingStatus); } }; } @@ -272,8 +273,8 @@ export function copyToClipboard(usageKey) { throw new Error(`Unexpected clipboard status "${clipboardData.content?.status}" in successful API response.`); } } catch (error) { - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); logError('Error copying to clipboard:', error); + handleResponseErrors(error, dispatch, updateSavingStatus); } finally { dispatch(hideProcessingNotification()); } @@ -296,7 +297,7 @@ export function setXBlockOrderListQuery(blockId, xblockListIds, restoreCallback) }); } catch (error) { restoreCallback(); - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + handleResponseErrors(error, dispatch, updateSavingStatus); } finally { dispatch(hideProcessingNotification()); } diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 251fcc828b..7d5977a3c2 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -20,6 +20,7 @@ import { getCourseUnitData, getIsLoading, getSavingStatus, + getErrorMessage, getSequenceStatus, getStaticFileNotices, getCanEdit, @@ -33,18 +34,16 @@ import { PUBLISH_TYPES } from './constants'; export const useCourseUnit = ({ courseId, blockId }) => { const dispatch = useDispatch(); - const [isErrorAlert, toggleErrorAlert] = useState(false); - const [hasInternetConnectionError, setInternetConnectionError] = useState(false); const courseUnit = useSelector(getCourseUnitData); const savingStatus = useSelector(getSavingStatus); const isLoading = useSelector(getIsLoading); + const errorMessage = useSelector(getErrorMessage); const sequenceStatus = useSelector(getSequenceStatus); const { draftPreviewLink, publishedPreviewLink } = useSelector(getCourseSectionVertical); const courseVerticalChildren = useSelector(getCourseVerticalChildren); const staticFileNotices = useSelector(getStaticFileNotices); const navigate = useNavigate(); const isTitleEditFormOpen = useSelector(state => state.courseUnit.isTitleEditFormOpen); - const isQueryPending = useSelector(state => state.courseUnit.isQueryPending); const canEdit = useSelector(getCanEdit); const { currentlyVisibleToStudents } = courseUnit; const { sharedClipboardData, showPasteXBlock, showPasteUnit } = useCopyToClipboard(canEdit); @@ -62,10 +61,6 @@ export const useCourseUnit = ({ courseId, blockId }) => { }, }; - const handleInternetConnectionFailed = () => { - setInternetConnectionError(true); - }; - const handleTitleEdit = () => { dispatch(changeEditTitleFormOpen(!isTitleEditFormOpen)); }; @@ -109,8 +104,6 @@ export const useCourseUnit = ({ courseId, blockId }) => { useEffect(() => { if (savingStatus === RequestStatus.SUCCESSFUL) { dispatch(updateQueryPendingStatus(true)); - } else if (savingStatus === RequestStatus.FAILED && !hasInternetConnectionError) { - toggleErrorAlert(true); } }, [savingStatus]); @@ -126,19 +119,16 @@ export const useCourseUnit = ({ courseId, blockId }) => { sequenceId, courseUnit, unitTitle, + errorMessage, sequenceStatus, savingStatus, - isQueryPending, - isErrorAlert, staticFileNotices, currentlyVisibleToStudents, isLoading, isTitleEditFormOpen, - isInternetConnectionAlertFailed: savingStatus === RequestStatus.FAILED, sharedClipboardData, showPasteXBlock, showPasteUnit, - handleInternetConnectionFailed, unitXBlockActions, headerNavigationsActions, handleTitleEdit, diff --git a/src/generic/saving-error-notification/SavingErrorNotification.jsx b/src/generic/saving-error-notification/SavingErrorNotification.jsx new file mode 100644 index 0000000000..c7f6aec1cb --- /dev/null +++ b/src/generic/saving-error-notification/SavingErrorNotification.jsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Warning as WarningIcon } from '@openedx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { RequestStatus } from '../../data/constants'; +import AlertMessage from '../alert-message'; +import messages from './messages'; + +const SavingErrorNotification = ({ + savingStatus, + errorMessage, +}) => { + const intl = useIntl(); + const [showAlert, setShowAlert] = useState(false); + const [isOnline, setIsOnline] = useState(window.navigator.onLine); + const isQueryFailed = savingStatus === RequestStatus.FAILED; + + useEffect(() => { + const handleOnlineStatus = () => setIsOnline(window.navigator.onLine); + const events = ['online', 'offline']; + + events.forEach((event) => { + window.addEventListener(event, handleOnlineStatus); + }); + + return () => { + events.forEach((event) => { + window.removeEventListener(event, handleOnlineStatus); + }); + }; + }, [isOnline]); + + useEffect(() => { + setShowAlert((!isOnline && isQueryFailed) || isQueryFailed); + }, [isQueryFailed, isOnline]); + + return ( +