From 3f987f9958120ceadb924e624ec70523e45a1ffb Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Wed, 22 May 2024 14:28:53 -0400 Subject: [PATCH] feat: improve error messaging and empty updates (#1025) * feat: improve error messaging and empty updates * chore: improve code coverage * fix: update error messages * fix: message title for saving handouts --- src/CourseAuthoringPage.jsx | 2 +- src/course-updates/CourseUpdates.jsx | 142 +++++-- src/course-updates/CourseUpdates.test.jsx | 438 +++++++++++++++------- src/course-updates/data/selectors.js | 1 + src/course-updates/data/slice.js | 16 +- src/course-updates/data/thunk.js | 62 ++- src/course-updates/hooks.jsx | 2 +- src/course-updates/messages.js | 69 ++++ src/index.scss | 8 + 9 files changed, 547 insertions(+), 193 deletions(-) diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx index c442f9406..c4281a8c1 100644 --- a/src/CourseAuthoringPage.jsx +++ b/src/CourseAuthoringPage.jsx @@ -67,7 +67,7 @@ const CourseAuthoringPage = ({ courseId, children }) => { ); } return ( -
+
{/* While V2 Editors are temporarily served from their own pages using url pattern containing /editor/, we shouldn't have the header and footer on these pages. diff --git a/src/course-updates/CourseUpdates.jsx b/src/course-updates/CourseUpdates.jsx index 791072730..a6b7af677 100644 --- a/src/course-updates/CourseUpdates.jsx +++ b/src/course-updates/CourseUpdates.jsx @@ -1,12 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { Helmet } from 'react-helmet'; import { useIntl } from '@edx/frontend-platform/i18n'; import { + ActionRow, Button, Container, Layout, } from '@openedx/paragon'; -import { Add as AddIcon } from '@openedx/paragon/icons'; +import { Add as AddIcon, ErrorOutline as ErrorIcon } from '@openedx/paragon/icons'; import { useSelector } from 'react-redux'; import { useModel } from '../generic/model-store'; @@ -22,15 +24,18 @@ import UpdateForm from './update-form/UpdateForm'; import { REQUEST_TYPES } from './constants'; import messages from './messages'; import { useCourseUpdates } from './hooks'; -import { getLoadingStatuses, getSavingStatuses } from './data/selectors'; +import { + getErrors, + getLoadingStatuses, + getSavingStatuses, +} from './data/selectors'; import { matchesAnyStatus } from './utils'; import getPageHeadTitle from '../generic/utils'; +import AlertMessage from '../generic/alert-message'; const CourseUpdates = ({ courseId }) => { const intl = useIntl(); - const courseDetails = useModel('courseDetails', courseId); - document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle)); const { requestType, @@ -56,6 +61,7 @@ const CourseUpdates = ({ courseId }) => { const loadingStatuses = useSelector(getLoadingStatuses); const savingStatuses = useSelector(getSavingStatuses); + const errors = useSelector(getErrors); const anyStatusFailed = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.FAILED); const anyStatusInProgress = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.IN_PROGRESS); @@ -63,8 +69,61 @@ const CourseUpdates = ({ courseId }) => { return ( <> - -
+ + + {getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle))} + + + +
+ {errors.loadingUpdates && ( + + )} + {errors.loadingHandouts && ( + + )} + {errors.creatingUpdate && ( + + )} + {errors.savingUpdates && ( + + )} + {errors.deletingUpdates && ( + + )} + {errors.savingHandouts && ( + + )} { xs={[{ span: 12 }]} xl={[{ span: 12 }]} > - +
{ iconBefore={AddIcon} size="sm" onClick={() => handleOpenUpdateForm(REQUEST_TYPES.add_new_update)} - disabled={isUpdateFormOpen} + disabled={isUpdateFormOpen || errors.loadingUpdates} > {intl.formatMessage(messages.newUpdateButton)} @@ -102,33 +161,54 @@ const CourseUpdates = ({ courseId }) => { /> )}
-
- {courseUpdates.length ? courseUpdates.map((courseUpdate, index) => ( - isInnerFormOpen(courseUpdate.id) ? ( - - ) : ( - handleOpenUpdateForm(REQUEST_TYPES.edit_update, courseUpdate)} - onDelete={() => handleOpenDeleteForm(courseUpdate)} - isDisabledButtons={isUpdateFormOpen} - /> - ))) : null} -
+ {courseUpdates.length > 0 && ( +
+ {courseUpdates.map((courseUpdate, index) => ( + isInnerFormOpen(courseUpdate.id) ? ( + + ) : ( + handleOpenUpdateForm(REQUEST_TYPES.edit_update, courseUpdate)} + onDelete={() => handleOpenDeleteForm(courseUpdate)} + isDisabledButtons={isUpdateFormOpen} + /> + ) + ))} +
+ )} + {!courseUpdates.length && ( + + + + {intl.formatMessage(messages.noCourseUpdates)} + + + + + )}
handleOpenUpdateForm(REQUEST_TYPES.edit_handouts)} - isDisabledButtons={isUpdateFormOpen} + isDisabledButtons={isUpdateFormOpen || errors.loadingHandouts} />
({ const RootWrapper = () => ( - + ); describe('', () => { - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, + describe('Successful API responses', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getCourseUpdatesApiUrl(courseId)) + .reply(200, courseUpdatesMock); + axiosMock + .onGet(getCourseHandoutApiUrl(courseId)) + .reply(200, courseHandoutsMock); }); - store = initializeStore(); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock - .onGet(getCourseUpdatesApiUrl(courseId)) - .reply(200, courseUpdatesMock); - axiosMock - .onGet(getCourseHandoutApiUrl(courseId)) - .reply(200, courseHandoutsMock); - }); + it('render CourseUpdates component correctly', async () => { + const { + getByText, getAllByTestId, getByTestId, getByRole, + } = render(); - it('render CourseUpdates component correctly', async () => { - const { - getByText, getAllByTestId, getByTestId, getByRole, - } = render(); - - await waitFor(() => { - expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.sectionInfo.defaultMessage)).toBeInTheDocument(); - expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeInTheDocument(); - expect(getAllByTestId('course-update')).toHaveLength(3); - expect(getByTestId('course-handouts')).toBeInTheDocument(); + await waitFor(() => { + expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.sectionInfo.defaultMessage)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeInTheDocument(); + expect(getAllByTestId('course-update')).toHaveLength(3); + expect(getByTestId('course-handouts')).toBeInTheDocument(); + }); }); - }); - it('should create course update', async () => { - const { getByText } = render(); + it('should create course update', async () => { + const { getByText } = render(); - const data = { - content: '

Some text

', - date: 'August 29, 2023', - }; + const data = { + content: '

Some text

', + date: 'August 29, 2023', + }; - axiosMock - .onPost(getCourseUpdatesApiUrl(courseId)) - .reply(200, data); + axiosMock + .onPost(getCourseUpdatesApiUrl(courseId)) + .reply(200, data); - await executeThunk(createCourseUpdateQuery(courseId, data), store.dispatch); - expect(getByText('Some text')).toBeInTheDocument(); - expect(getByText(data.date)).toBeInTheDocument(); - }); - - it('should edit course update', async () => { - const { getByText, queryByText } = render(); - - const data = { - id: courseUpdatesMock[0].id, - content: '

Some text

', - date: 'August 29, 2023', - }; - - axiosMock - .onPut(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id)) - .reply(200, data); - - await executeThunk(editCourseUpdateQuery(courseId, data), store.dispatch); - expect(getByText('Some text')).toBeInTheDocument(); - expect(getByText(data.date)).toBeInTheDocument(); - expect(queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument(); - expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument(); - }); - - it('should delete course update', async () => { - const { queryByText } = render(); - - axiosMock - .onDelete(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id)) - .reply(200); - - await executeThunk(deleteCourseUpdateQuery(courseId, courseUpdatesMock[0].id), store.dispatch); - expect(queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument(); - expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument(); - }); - - it('should edit course handouts', async () => { - const { getByText, queryByText } = render(); - - const data = { - ...courseHandoutsMock, - data: '

Some handouts 1

', - }; - - axiosMock - .onPut(getCourseHandoutApiUrl(courseId)) - .reply(200, data); - - await executeThunk(editCourseHandoutsQuery(courseId, data), store.dispatch); - expect(getByText('Some handouts 1')).toBeInTheDocument(); - expect(queryByText(courseHandoutsMock.data)).not.toBeInTheDocument(); - }); - - it('Add new update form is visible after clicking "New update" button', async () => { - const { getByText, getByRole, getAllByTestId } = render(); - - await waitFor(() => { - const editUpdateButtons = getAllByTestId('course-update-edit-button'); - const deleteButtons = getAllByTestId('course-update-delete-button'); - const editHandoutsButtons = getAllByTestId('course-handouts-edit-button'); - const newUpdateButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage }); - - fireEvent.click(newUpdateButton); - - expect(newUpdateButton).toBeDisabled(); - editUpdateButtons.forEach((button) => expect(button).toBeDisabled()); - editHandoutsButtons.forEach((button) => expect(button).toBeDisabled()); - deleteButtons.forEach((button) => expect(button).toBeDisabled()); - expect(getByText('Add new update')).toBeInTheDocument(); + await executeThunk(createCourseUpdateQuery(courseId, data), store.dispatch); + expect(getByText('Some text')).toBeInTheDocument(); + expect(getByText(data.date)).toBeInTheDocument(); }); - }); - it('Edit handouts form is visible after clicking "Edit" button', async () => { - const { getByText, getByRole, getAllByTestId } = render(); + it('should edit course update', async () => { + const { getByText, queryByText } = render(); - await waitFor(() => { - const editUpdateButtons = getAllByTestId('course-update-edit-button'); - const deleteButtons = getAllByTestId('course-update-delete-button'); - const editHandoutsButtons = getAllByTestId('course-handouts-edit-button'); - const editHandoutsButton = editHandoutsButtons[0]; + const data = { + id: courseUpdatesMock[0].id, + content: '

Some text

', + date: 'August 29, 2023', + }; - fireEvent.click(editHandoutsButton); + axiosMock + .onPut(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id)) + .reply(200, data); - expect(editHandoutsButton).toBeDisabled(); - expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled(); - editUpdateButtons.forEach((button) => expect(button).toBeDisabled()); - editHandoutsButtons.forEach((button) => expect(button).toBeDisabled()); - deleteButtons.forEach((button) => expect(button).toBeDisabled()); - expect(getByText('Edit handouts')).toBeInTheDocument(); - }); - }); - - it('Edit update form is visible after clicking "Edit" button', async () => { - const { - getByText, getByRole, getAllByTestId, queryByText, - } = render(); - - await waitFor(() => { - const editUpdateButtons = getAllByTestId('course-update-edit-button'); - const deleteButtons = getAllByTestId('course-update-delete-button'); - const editHandoutsButtons = getAllByTestId('course-handouts-edit-button'); - const editUpdateFirstButton = editUpdateButtons[0]; - - fireEvent.click(editUpdateFirstButton); - expect(getByText('Edit update')).toBeInTheDocument(); - expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled(); - editUpdateButtons.forEach((button) => expect(button).toBeDisabled()); - editHandoutsButtons.forEach((button) => expect(button).toBeDisabled()); - deleteButtons.forEach((button) => expect(button).toBeDisabled()); + await executeThunk(editCourseUpdateQuery(courseId, data), store.dispatch); + expect(getByText('Some text')).toBeInTheDocument(); + expect(getByText(data.date)).toBeInTheDocument(); + expect(queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument(); expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument(); }); + + it('should delete course update', async () => { + const { queryByText } = render(); + + axiosMock + .onDelete(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id)) + .reply(200); + + await executeThunk(deleteCourseUpdateQuery(courseId, courseUpdatesMock[0].id), store.dispatch); + expect(queryByText(courseUpdatesMock[0].date)).not.toBeInTheDocument(); + expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument(); + }); + + it('should edit course handouts', async () => { + const { getByText, queryByText } = render(); + + const data = { + ...courseHandoutsMock, + data: '

Some handouts 1

', + }; + + axiosMock + .onPut(getCourseHandoutApiUrl(courseId)) + .reply(200, data); + + await executeThunk(editCourseHandoutsQuery(courseId, data), store.dispatch); + expect(getByText('Some handouts 1')).toBeInTheDocument(); + expect(queryByText(courseHandoutsMock.data)).not.toBeInTheDocument(); + }); + + it('Add new update form is visible after clicking "New update" button', async () => { + const { getByText, getByRole, getAllByTestId } = render(); + + await waitFor(() => { + const editUpdateButtons = getAllByTestId('course-update-edit-button'); + const deleteButtons = getAllByTestId('course-update-delete-button'); + const editHandoutsButtons = getAllByTestId('course-handouts-edit-button'); + const newUpdateButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage }); + + fireEvent.click(newUpdateButton); + + expect(newUpdateButton).toBeDisabled(); + editUpdateButtons.forEach((button) => expect(button).toBeDisabled()); + editHandoutsButtons.forEach((button) => expect(button).toBeDisabled()); + deleteButtons.forEach((button) => expect(button).toBeDisabled()); + expect(getByText('Add new update')).toBeInTheDocument(); + }); + }); + + it('Edit handouts form is visible after clicking "Edit" button', async () => { + const { getByText, getByRole, getAllByTestId } = render(); + + await waitFor(() => { + const editUpdateButtons = getAllByTestId('course-update-edit-button'); + const deleteButtons = getAllByTestId('course-update-delete-button'); + const editHandoutsButtons = getAllByTestId('course-handouts-edit-button'); + const editHandoutsButton = editHandoutsButtons[0]; + + fireEvent.click(editHandoutsButton); + + expect(editHandoutsButton).toBeDisabled(); + expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled(); + editUpdateButtons.forEach((button) => expect(button).toBeDisabled()); + editHandoutsButtons.forEach((button) => expect(button).toBeDisabled()); + deleteButtons.forEach((button) => expect(button).toBeDisabled()); + expect(getByText('Edit handouts')).toBeInTheDocument(); + }); + }); + + it('Edit update form is visible after clicking "Edit" button', async () => { + const { + getByText, getByRole, getAllByTestId, queryByText, + } = render(); + + await waitFor(() => { + const editUpdateButtons = getAllByTestId('course-update-edit-button'); + const deleteButtons = getAllByTestId('course-update-delete-button'); + const editHandoutsButtons = getAllByTestId('course-handouts-edit-button'); + const editUpdateFirstButton = editUpdateButtons[0]; + + fireEvent.click(editUpdateFirstButton); + expect(getByText('Edit update')).toBeInTheDocument(); + expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled(); + editUpdateButtons.forEach((button) => expect(button).toBeDisabled()); + editHandoutsButtons.forEach((button) => expect(button).toBeDisabled()); + deleteButtons.forEach((button) => expect(button).toBeDisabled()); + expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument(); + }); + }); + }); + + describe('page load failure API responses', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + it('Course updates fetch should show updates loading error', async () => { + axiosMock + .onGet(getCourseUpdatesApiUrl(courseId)) + .reply(404); + axiosMock + .onGet(getCourseHandoutApiUrl(courseId)) + .reply(200, courseHandoutsMock); + + const { + getByText, queryByTestId, getByRole, + } = render(); + + await waitFor(() => { + const newButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage }); + expect(getByText(messages.loadingUpdatesErrorTitle.defaultMessage)); + expect(newButton).toBeDisabled(); + expect(getByText(messages.noCourseUpdates.defaultMessage)).toBeVisible(); + expect(queryByTestId('course-update')).toBeNull(); + }); + }); + + it('Course handouts fetch should show handouts loading error', async () => { + axiosMock + .onGet(getCourseUpdatesApiUrl(courseId)) + .reply(200, courseUpdatesMock); + axiosMock + .onGet(getCourseHandoutApiUrl(courseId)) + .reply(404); + + const { + getByText, getByTestId, + } = render(); + + await waitFor(() => { + expect(getByText(messages.loadingHandoutsErrorTitle.defaultMessage)); + expect(getByTestId('course-handouts-edit-button')).toBeDisabled(); + }); + }); + }); + + describe('saving failure API responses', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getCourseUpdatesApiUrl(courseId)) + .reply(200, courseUpdatesMock); + axiosMock + .onGet(getCourseHandoutApiUrl(courseId)) + .reply(200, courseHandoutsMock); + }); + it('creating new update should show saving error alert', async () => { + const { getByText, queryByText } = render(); + + const data = { + content: '

Some text

', + date: 'August 29, 2023', + }; + + axiosMock + .onPost(getCourseUpdatesApiUrl(courseId), data) + .reply(404); + + await executeThunk(createCourseUpdateQuery(courseId, data), store.dispatch); + expect(getByText(messages.savingNewUpdateErrorAlertDescription.defaultMessage)).toBeVisible(); + expect(queryByText('Some text')).toBeNull(); + expect(queryByText(data.date)).toBeNull(); + }); + + it('editing course update should show saving error alert', async () => { + const { getByText, queryByText } = render(); + + const data = { + id: courseUpdatesMock[0].id, + content: '

Some text

', + date: 'August 29, 2023', + }; + + axiosMock + .onPut(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id)) + .reply(404); + + await executeThunk(editCourseUpdateQuery(courseId, data), store.dispatch); + expect(queryByText('Some text')).toBeNull(); + expect(queryByText(data.date)).toBeNull(); + expect(getByText(courseUpdatesMock[0].date)).toBeVisible(); + expect(getByText(courseUpdatesMock[0].content)).toBeVisible(); + expect(getByText(messages.savingUpdatesErrorDescription.defaultMessage)).toBeVisible(); + }); + + it('deleting course update should show delete saving error alert', async () => { + const { getByText } = render(); + + axiosMock + .onDelete(updateCourseUpdatesApiUrl(courseId, courseUpdatesMock[0].id)) + .reply(404); + + await executeThunk(deleteCourseUpdateQuery(courseId, courseUpdatesMock[0].id), store.dispatch); + expect(getByText(courseUpdatesMock[0].date)).toBeVisible(); + expect(getByText(courseUpdatesMock[0].content)).toBeVisible(); + expect(getByText(messages.deletingUpdatesErrorDescription.defaultMessage)).toBeVisible(); + }); + + it('editing course handouts should show saving error alert', async () => { + const { getByText, queryByText } = render(); + + const data = { + ...courseHandoutsMock, + data: '

Some handouts 1

', + }; + + axiosMock + .onPut(getCourseHandoutApiUrl(courseId)) + .reply(404); + + await executeThunk(editCourseHandoutsQuery(courseId, data), store.dispatch); + expect(queryByText('Some handouts 1')).toBeNull(); + expect(getByText(courseHandoutsMock.data)).toBeVisible(); + expect(getByText(messages.savingHandoutsErrorDescription.defaultMessage)); + }); }); }); diff --git a/src/course-updates/data/selectors.js b/src/course-updates/data/selectors.js index 947ad0f8a..b062f6748 100644 --- a/src/course-updates/data/selectors.js +++ b/src/course-updates/data/selectors.js @@ -2,3 +2,4 @@ export const getCourseUpdates = (state) => state.courseUpdates.courseUpdates; export const getCourseHandouts = (state) => state.courseUpdates.courseHandouts; export const getSavingStatuses = (state) => state.courseUpdates.savingStatuses; export const getLoadingStatuses = (state) => state.courseUpdates.loadingStatuses; +export const getErrors = (state) => state.courseUpdates.errors; diff --git a/src/course-updates/data/slice.js b/src/course-updates/data/slice.js index 18cd86a1a..dbb130fcf 100644 --- a/src/course-updates/data/slice.js +++ b/src/course-updates/data/slice.js @@ -15,6 +15,14 @@ const initialState = { fetchCourseUpdatesQuery: '', fetchCourseHandoutsQuery: '', }, + errors: { + creatingUpdate: false, + deletingUpdates: false, + loadingUpdates: false, + loadingHandouts: false, + savingUpdates: false, + savingHandouts: false, + }, }; const slice = createSlice({ @@ -48,10 +56,14 @@ const slice = createSlice({ }; }, updateSavingStatuses: (state, { payload }) => { - state.savingStatuses = { ...state.savingStatuses, ...payload }; + const { status, error } = payload; + state.errors = { ...initialState.errors, ...error }; + state.savingStatuses = { ...state.savingStatuses, ...status }; }, updateLoadingStatuses: (state, { payload }) => { - state.loadingStatuses = { ...state.loadingStatuses, ...payload }; + const { status, error } = payload; + state.errors = { ...initialState.errors, ...error }; + state.loadingStatuses = { ...state.loadingStatuses, ...status }; }, }, }); diff --git a/src/course-updates/data/thunk.js b/src/course-updates/data/thunk.js index 8713b808c..88b3a0578 100644 --- a/src/course-updates/data/thunk.js +++ b/src/course-updates/data/thunk.js @@ -23,12 +23,18 @@ import { export function fetchCourseUpdatesQuery(courseId) { return async (dispatch) => { try { - dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.IN_PROGRESS })); + dispatch(updateLoadingStatuses({ fetchCourseUpdatesQuery: RequestStatus.IN_PROGRESS })); const courseUpdates = await getCourseUpdates(courseId); dispatch(fetchCourseUpdatesSuccess(courseUpdates)); - dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.SUCCESSFUL })); + dispatch(updateLoadingStatuses({ + status: { fetchCourseUpdatesQuery: RequestStatus.SUCCESSFUL }, + error: { loadingUpdates: false }, + })); } catch (error) { - dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.FAILED })); + dispatch(updateLoadingStatuses({ + status: { fetchCourseUpdatesQuery: RequestStatus.FAILED }, + error: { loadingUpdates: true }, + })); } }; } @@ -41,10 +47,16 @@ export function createCourseUpdateQuery(courseId, data) { const courseUpdate = await createUpdate(courseId, data); dispatch(createCourseUpdate(courseUpdate)); dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.SUCCESSFUL }, + error: { creatingUpdate: false }, + })); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.FAILED }, + error: { creatingUpdate: true }, + })); } }; } @@ -57,10 +69,16 @@ export function editCourseUpdateQuery(courseId, data) { const courseUpdate = await editUpdate(courseId, data); dispatch(editCourseUpdate(courseUpdate)); dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.SUCCESSFUL }, + error: { savingUpdates: false }, + })); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.FAILED }, + error: { savingUpdates: true }, + })); } }; } @@ -73,10 +91,16 @@ export function deleteCourseUpdateQuery(courseId, updateId) { const courseUpdates = await deleteUpdate(courseId, updateId); dispatch(deleteCourseUpdate(courseUpdates)); dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.SUCCESSFUL }, + error: { deletingUpdates: false }, + })); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.FAILED }, + error: { deletingUpdates: true }, + })); } }; } @@ -87,9 +111,15 @@ export function fetchCourseHandoutsQuery(courseId) { dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.IN_PROGRESS })); const courseHandouts = await getCourseHandouts(courseId); dispatch(fetchCourseHandoutsSuccess(courseHandouts)); - dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.SUCCESSFUL })); + dispatch(updateLoadingStatuses({ + status: { fetchCourseHandoutsQuery: RequestStatus.SUCCESSFUL }, + error: { loadingHandouts: false }, + })); } catch (error) { - dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.FAILED })); + dispatch(updateLoadingStatuses({ + status: { fetchCourseHandoutsQuery: RequestStatus.FAILED }, + error: { loadingHandouts: true }, + })); } }; } @@ -102,10 +132,16 @@ export function editCourseHandoutsQuery(courseId, data) { const courseHandouts = await editHandouts(courseId, data); dispatch(editCourseHandouts(courseHandouts)); dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.SUCCESSFUL }, + error: { savingHandouts: false }, + })); } catch (error) { dispatch(hideProcessingNotification()); - dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + dispatch(updateSavingStatuses({ + status: { createCourseUpdateQuery: RequestStatus.FAILED }, + error: { savingHandouts: true }, + })); } }; } diff --git a/src/course-updates/hooks.jsx b/src/course-updates/hooks.jsx index 05f80fe03..0482af6f2 100644 --- a/src/course-updates/hooks.jsx +++ b/src/course-updates/hooks.jsx @@ -98,7 +98,7 @@ const useCourseUpdates = ({ courseId }) => { courseHandouts, courseUpdatesInitialValues, isMainFormOpen: isUpdateFormOpen && requestType !== REQUEST_TYPES.edit_update, - isInnerFormOpen: (id) => isUpdateFormOpen && currentUpdate.id === id && requestType === REQUEST_TYPES.edit_update, + isInnerFormOpen: (id) => (isUpdateFormOpen && currentUpdate.id === id && requestType === REQUEST_TYPES.edit_update), isUpdateFormOpen, isDeleteModalOpen, closeUpdateForm, diff --git a/src/course-updates/messages.js b/src/course-updates/messages.js index a04a4b5ce..a148972ae 100644 --- a/src/course-updates/messages.js +++ b/src/course-updates/messages.js @@ -4,18 +4,87 @@ const messages = defineMessages({ headingTitle: { id: 'course-authoring.course-updates.header.title', defaultMessage: 'Course updates', + description: 'Title for page', }, headingSubtitle: { id: 'course-authoring.course-updates.header.subtitle', defaultMessage: 'Content', + description: 'Subtitle for page', }, sectionInfo: { id: 'course-authoring.course-updates.section-info', defaultMessage: 'Use course updates to notify students of important dates or exams, highlight particular discussions in the forums, announce schedule changes, and respond to student questions.', + description: 'Message describing the use of course updates in a course', }, newUpdateButton: { id: 'course-authoring.course-updates.actions.new-update', defaultMessage: 'New update', + description: 'Button label for header button to add a new course update', + }, + firstUpdateButton: { + id: 'course-authoring.course-updates.actions.first-update', + defaultMessage: 'Add first update', + description: 'Button label for button to add first course update', + }, + noCourseUpdates: { + id: 'course-authoring.course-updates.actions.first-update-message', + defaultMessage: 'You have not added any updates to this course yet.', + description: 'Message to notify user that they do not have any existing course updates', + }, + loadingUpdatesErrorTitle: { + id: 'course-authoring.course-updates.error.loading-updates.title', + defaultMessage: 'Failed to load course updates', + description: 'Alert title for loading updates error alert', + }, + loadingUpdatesErrorDescription: { + id: 'course-authoring.course-updates.error.loading-updates.description', + defaultMessage: 'Failed to load course updates for {courseId}. Please try again later.', + description: 'Alert body message for loading course update errors', + }, + loadingHandoutsErrorTitle: { + id: 'course-authoring.course-updates.error.loading-handouts.title', + defaultMessage: 'Failed to load course handouts', + description: 'Alert title for loading handouts error alert', + }, + loadingHandoutsErrorDescription: { + id: 'course-authoring.course-updates.error.loading-handouts.description', + defaultMessage: 'Failed to load course updates for {courseId}. Please try again later.', + description: 'Alert body message for loading course handout errors', + }, + savingUpdatesErrorTitle: { + id: 'course-authoring.course-updates.error.saving-updates.title', + defaultMessage: 'Failed to save course update', + description: 'Alert title for saving updates error alert', + }, + savingUpdatesErrorDescription: { + id: 'course-authoring.course-updates.error.saving-updates.description', + defaultMessage: 'Failed to save recent changes to course update. Please try again later.', + description: 'Alert body message for saving edits to course update errors', + }, + savingNewUpdateErrorAlertDescription: { + id: 'course-authoring.course-updates.error.saving-new-updates.description', + defaultMessage: 'Failed to save new course update. Please try again later.', + description: 'Alert body message for saving new course update errors', + }, + savingHandoutsErrorTitle: { + id: 'course-authoring.course-updates.error.saving-handouts.title', + defaultMessage: 'Failed to save course handouts', + description: 'Alert title for saving handouts error alert', + }, + savingHandoutsErrorDescription: { + id: 'course-authoring.course-updates.error.saving-handouts.description', + defaultMessage: 'Failed to save recent changes to course handouts. Please try again later.', + description: 'Alert body message for saving course handout errors', + }, + deletingUpdatesErrorTitle: { + id: 'course-authoring.course-updates.error.deleting-updates.title', + defaultMessage: 'Failed to delete course update', + description: 'Alert title for deleting update error alert', + }, + deletingUpdatesErrorDescription: { + id: 'course-authoring.course-updates.error.deleting-updates.description', + defaultMessage: 'Failed to delete selected course update. Please try again later.', + description: 'Alert body message for deleting course update errors', }, }); diff --git a/src/index.scss b/src/index.scss index 5409c7085..912b40933 100644 --- a/src/index.scss +++ b/src/index.scss @@ -51,3 +51,11 @@ div.xblock-highlight { box-shadow: unset; } } + +body { + background-color: $light-200; + + .editor-page { + background-color: $light-100; + } +}