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,
)}
/>
+
+
{intl.formatMessage(messages.headingSubtitle)}
diff --git a/src/schedule-and-details/messages.js b/src/schedule-and-details/messages.js
index f3d6ad3e1..b2ba0533d 100644
--- a/src/schedule-and-details/messages.js
+++ b/src/schedule-and-details/messages.js
@@ -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.',