fix: error handling for save api call (#805)
This commit is contained in:
@@ -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('<ScheduleAndDetails />', () => {
|
||||
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('<ScheduleAndDetails />', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a success message when course details saves', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
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(<RootWrapper />);
|
||||
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(<RootWrapper />);
|
||||
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(<RootWrapper />);
|
||||
await executeThunk(updateCourseDetailsQuery(courseId, 'DaTa'), store.dispatch);
|
||||
expect(getByText(messages.alertFail.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
/>
|
||||
<AlertMessage
|
||||
show={showLoadFailedAlert}
|
||||
variant="danger"
|
||||
icon={ErrorOutlineIcon}
|
||||
title={intl.formatMessage(messages.alertLoadFail)}
|
||||
aria-hidden="true"
|
||||
aria-labelledby={intl.formatMessage(
|
||||
messages.alertFailAriaLabelledby,
|
||||
)}
|
||||
aria-describedby={intl.formatMessage(
|
||||
messages.alertFailAriaDescribedby,
|
||||
)}
|
||||
/>
|
||||
<AlertMessage
|
||||
show={showFailedAlert}
|
||||
variant="danger"
|
||||
icon={ErrorOutlineIcon}
|
||||
title={intl.formatMessage(messages.alertFail)}
|
||||
aria-hidden="true"
|
||||
aria-labelledby={intl.formatMessage(
|
||||
messages.alertFailAriaLabelledby,
|
||||
)}
|
||||
aria-describedby={intl.formatMessage(
|
||||
messages.alertFailAriaDescribedby,
|
||||
)}
|
||||
/>
|
||||
<header>
|
||||
<span className="small text-gray-700">
|
||||
{intl.formatMessage(messages.headingSubtitle)}
|
||||
|
||||
@@ -57,6 +57,30 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.schedule.alert.success',
|
||||
defaultMessage: 'Your changes have been saved.',
|
||||
},
|
||||
alertLoadFailAriaLabelledby: {
|
||||
id: 'course-authoring.schedule.alert.load.fail.aria.labelledby',
|
||||
defaultMessage: 'alert-confirmation-title',
|
||||
},
|
||||
alertLoadFailAriaDescribedby: {
|
||||
id: 'course-authoring.schedule.alert.load.fail.aria.describedby',
|
||||
defaultMessage: 'alert-confirmation-description',
|
||||
},
|
||||
alertLoadFail: {
|
||||
id: 'course-authoring.schedule.alert.load.fail',
|
||||
defaultMessage: 'We encountered an error when loading your settings.',
|
||||
},
|
||||
alertFailAriaLabelledby: {
|
||||
id: 'course-authoring.schedule.alert.fail.aria.labelledby',
|
||||
defaultMessage: 'alert-confirmation-title',
|
||||
},
|
||||
alertFailAriaDescribedby: {
|
||||
id: 'course-authoring.schedule.alert.fail.aria.describedby',
|
||||
defaultMessage: 'alert-confirmation-description',
|
||||
},
|
||||
alertFail: {
|
||||
id: 'course-authoring.schedule.alert.fail',
|
||||
defaultMessage: 'We encountered an error when saving your changes.',
|
||||
},
|
||||
errorMessage1: {
|
||||
id: 'course-authoring.schedule.schedule-section.error-message-1',
|
||||
defaultMessage: 'The certificates display behavior must be \'A date after the course end date\' if certificate available date is set.',
|
||||
|
||||
Reference in New Issue
Block a user