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(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(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(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(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(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(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(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; + } +}