From c2ad1b8c99510c78c4bd37c3185e4d3515a1e9ee Mon Sep 17 00:00:00 2001 From: Raymond Zhou <56318341+rayzhou-bit@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:55:24 -0800 Subject: [PATCH] fix: error handling for save api call (#805) --- .../ScheduleAndDetails.test.jsx | 43 +++++++++++++++++ src/schedule-and-details/hooks.jsx | 47 ++++++++++++++++++- src/schedule-and-details/index.jsx | 46 ++++++++++++++---- src/schedule-and-details/messages.js | 24 ++++++++++ 4 files changed, 150 insertions(+), 10 deletions(-) diff --git a/src/schedule-and-details/ScheduleAndDetails.test.jsx b/src/schedule-and-details/ScheduleAndDetails.test.jsx index a46cf2bb3..74215bae0 100644 --- a/src/schedule-and-details/ScheduleAndDetails.test.jsx +++ b/src/schedule-and-details/ScheduleAndDetails.test.jsx @@ -9,8 +9,10 @@ import { import MockAdapter from 'axios-mock-adapter'; import initializeStore from '../store'; +import { executeThunk } from '../utils'; import { courseDetailsMock, courseSettingsMock } from './__mocks__'; import { getCourseDetailsApiUrl, getCourseSettingsApiUrl } from './data/api'; +import { updateCourseDetailsQuery } from './data/thunks'; import { DATE_FORMAT } from '../constants'; import creditMessages from './credit-section/messages'; import pacingMessages from './pacing-section/messages'; @@ -75,6 +77,12 @@ describe('', () => { axiosMock .onGet(getCourseSettingsApiUrl(courseId)) .reply(200, courseSettingsMock); + axiosMock + .onPut(getCourseDetailsApiUrl(courseId)) + .reply(200); + }); + afterEach(() => { + axiosMock.reset(); }); it('should render without errors', async () => { @@ -136,4 +144,39 @@ describe('', () => { ).toBeInTheDocument(); }); }); + + it('should display a success message when course details saves', async () => { + const { getByText } = render(); + await executeThunk(updateCourseDetailsQuery(courseId, 'DaTa'), store.dispatch); + expect(getByText(messages.alertSuccess.defaultMessage)).toBeInTheDocument(); + }); + + it('should display an error when GET CourseDetails fails', async () => { + axiosMock + .onGet(getCourseDetailsApiUrl(courseId)) + .reply(404, 'error'); + const { getByText } = render(); + await waitFor(() => { + expect(getByText(messages.alertLoadFail.defaultMessage)).toBeInTheDocument(); + }); + }); + + it('should display an error when GET CourseSettings fails', async () => { + axiosMock + .onGet(getCourseSettingsApiUrl(courseId)) + .reply(404, 'error'); + const { getByText } = render(); + await waitFor(() => { + expect(getByText(messages.alertLoadFail.defaultMessage)).toBeInTheDocument(); + }); + }); + + it('should display an error when PUT CourseDetails fails', async () => { + axiosMock + .onPut(getCourseDetailsApiUrl(courseId)) + .reply(404, 'error'); + const { getByText } = render(); + await executeThunk(updateCourseDetailsQuery(courseId, 'DaTa'), store.dispatch); + expect(getByText(messages.alertFail.defaultMessage)).toBeInTheDocument(); + }); }); diff --git a/src/schedule-and-details/hooks.jsx b/src/schedule-and-details/hooks.jsx index 1eafb4475..01887a3b0 100644 --- a/src/schedule-and-details/hooks.jsx +++ b/src/schedule-and-details/hooks.jsx @@ -3,9 +3,36 @@ import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; import { RequestStatus } from '../data/constants'; -import { getSavingStatus } from './data/selectors'; +import { getLoadingDetailsStatus, getLoadingSettingsStatus, getSavingStatus } from './data/selectors'; import { validateScheduleAndDetails, updateWithDefaultValues } from './utils'; +const useLoadValuesPrompt = ( + courseId, + fetchCourseDetailsQuery, + fetchCourseSettingsQuery, +) => { + const dispatch = useDispatch(); + const loadingDetailsStatus = useSelector(getLoadingDetailsStatus); + const loadingSettingsStatus = useSelector(getLoadingSettingsStatus); + const [showLoadFailedAlert, setShowLoadFailedAlert] = useState(false); + + useEffect(() => { + dispatch(fetchCourseDetailsQuery(courseId)); + dispatch(fetchCourseSettingsQuery(courseId)); + }, [courseId]); + + useEffect(() => { + if (loadingDetailsStatus === RequestStatus.FAILED || loadingSettingsStatus === RequestStatus.FAILED) { + setShowLoadFailedAlert(true); + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [loadingDetailsStatus, loadingSettingsStatus]); + + return { + showLoadFailedAlert, + }; +}; + const useSaveValuesPrompt = ( courseId, updateDataQuery, @@ -17,6 +44,7 @@ const useSaveValuesPrompt = ( const savingStatus = useSelector(getSavingStatus); const [editedValues, setEditedValues] = useState(initialEditedData); const [showSuccessfulAlert, setShowSuccessfulAlert] = useState(false); + const [showFailedAlert, setShowFailedAlert] = useState(false); const [showModifiedAlert, setShowModifiedAlert] = useState(false); const [isQueryPending, setIsQueryPending] = useState(false); const [isEditableState, setIsEditableState] = useState(false); @@ -36,6 +64,7 @@ const useSaveValuesPrompt = ( const handleValuesChange = (value, fieldName) => { setIsEditableState(true); setShowSuccessfulAlert(false); + setShowFailedAlert(false); if (editedValues[fieldName] !== value) { setEditedValues((prevEditedValues) => ({ @@ -54,6 +83,7 @@ const useSaveValuesPrompt = ( setEditedValues(initialEditedData || {}); setShowModifiedAlert(false); setShowSuccessfulAlert(false); + setShowFailedAlert(false); }; const handleUpdateValues = () => { @@ -64,11 +94,13 @@ const useSaveValuesPrompt = ( const handleInternetConnectionFailed = () => { setShowModifiedAlert(false); setShowSuccessfulAlert(false); + setShowFailedAlert(false); setIsQueryPending(false); }; const handleQueryProcessing = () => { setShowSuccessfulAlert(false); + setShowFailedAlert(false); dispatch(updateDataQuery(courseId, updateWithDefaultValues(editedValues))); }; @@ -76,9 +108,19 @@ const useSaveValuesPrompt = ( if (savingStatus === RequestStatus.SUCCESSFUL) { setIsQueryPending(false); setShowSuccessfulAlert(true); + setShowFailedAlert(false); setTimeout(() => setShowSuccessfulAlert(false), 15000); window.scrollTo({ top: 0, behavior: 'smooth' }); + if (!isEditableState) { + setShowModifiedAlert(false); + } + } else if (savingStatus === RequestStatus.FAILED) { + setIsQueryPending(false); + setShowSuccessfulAlert(false); + setShowFailedAlert(true); + window.scrollTo({ top: 0, behavior: 'smooth' }); + if (!isEditableState) { setShowModifiedAlert(false); } @@ -93,6 +135,7 @@ const useSaveValuesPrompt = ( isEditableState, showModifiedAlert, showSuccessfulAlert, + showFailedAlert, dispatch, setErrorFields, handleResetValues, @@ -104,4 +147,4 @@ const useSaveValuesPrompt = ( }; /* eslint-disable-next-line import/prefer-default-export */ -export { useSaveValuesPrompt }; +export { useLoadValuesPrompt, useSaveValuesPrompt }; diff --git a/src/schedule-and-details/index.jsx b/src/schedule-and-details/index.jsx index 06b66acbb..4fa68f93c 100644 --- a/src/schedule-and-details/index.jsx +++ b/src/schedule-and-details/index.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { @@ -6,6 +6,7 @@ import { } from '@edx/paragon'; import { CheckCircle as CheckCircleIcon, + ErrorOutline as ErrorOutlineIcon, Warning as WarningIcon, } from '@edx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -41,7 +42,7 @@ import RequirementsSection from './requirements-section'; import LicenseSection from './license-section'; import ScheduleSidebar from './schedule-sidebar'; import messages from './messages'; -import { useSaveValuesPrompt } from './hooks'; +import { useLoadValuesPrompt, useSaveValuesPrompt } from './hooks'; const ScheduleAndDetails = ({ intl, courseId }) => { const courseSettings = useSelector(getCourseSettings); @@ -76,6 +77,14 @@ const ScheduleAndDetails = ({ intl, courseId }) => { canShowCertificateAvailableDateField, } = courseSettings; + const { + showLoadFailedAlert, + } = useLoadValuesPrompt( + courseId, + fetchCourseDetailsQuery, + fetchCourseSettingsQuery, + ); + const { errorFields, savingStatus, @@ -84,7 +93,7 @@ const ScheduleAndDetails = ({ intl, courseId }) => { isEditableState, showModifiedAlert, showSuccessfulAlert, - dispatch, + showFailedAlert, handleResetValues, handleValuesChange, handleUpdateValues, @@ -129,11 +138,6 @@ const ScheduleAndDetails = ({ intl, courseId }) => { videoThumbnailImageAssetPath, } = editedValues; - useEffect(() => { - dispatch(fetchCourseSettingsQuery(courseId)); - dispatch(fetchCourseDetailsQuery(courseId)); - }, [courseId]); - useScrollToHashElement({ isLoading }); if (isLoading) { @@ -184,6 +188,32 @@ const ScheduleAndDetails = ({ intl, courseId }) => { messages.alertSuccessAriaDescribedby, )} /> +