diff --git a/.env b/.env index 2874b4a97..452df8210 100644 --- a/.env +++ b/.env @@ -32,7 +32,6 @@ ENABLE_TEAM_TYPE_SETTING=false ENABLE_NEW_EDITOR_PAGES=true ENABLE_NEW_HOME_PAGE = false ENABLE_NEW_COURSE_OUTLINE_PAGE = false -ENABLE_NEW_UPDATES_PAGE = false ENABLE_NEW_VIDEO_UPLOAD_PAGE = false ENABLE_NEW_GRADING_PAGE = false ENABLE_NEW_COURSE_TEAM_PAGE = false diff --git a/.env.development b/.env.development index b17611fad..184972f45 100644 --- a/.env.development +++ b/.env.development @@ -34,7 +34,6 @@ ENABLE_TEAM_TYPE_SETTING=false ENABLE_NEW_EDITOR_PAGES=true ENABLE_NEW_HOME_PAGE = false ENABLE_NEW_COURSE_OUTLINE_PAGE = false -ENABLE_NEW_UPDATES_PAGE = false ENABLE_NEW_VIDEO_UPLOAD_PAGE = false ENABLE_NEW_GRADING_PAGE = false ENABLE_NEW_COURSE_TEAM_PAGE = false diff --git a/.env.test b/.env.test index 8a5740394..f97e6c893 100644 --- a/.env.test +++ b/.env.test @@ -30,7 +30,6 @@ ENABLE_TEAM_TYPE_SETTING=false ENABLE_NEW_EDITOR_PAGES=true ENABLE_NEW_HOME_PAGE = false ENABLE_NEW_COURSE_OUTLINE_PAGE = true -ENABLE_NEW_UPDATES_PAGE = true ENABLE_NEW_VIDEO_UPLOAD_PAGE = true ENABLE_NEW_GRADING_PAGE = true ENABLE_NEW_COURSE_TEAM_PAGE = true diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index 854c0ee08..1d886b0f8 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -14,6 +14,7 @@ import { AdvancedSettings } from './advanced-settings'; import ScheduleAndDetails from './schedule-and-details'; import { GradingSettings } from './grading-settings'; import CourseTeam from './course-team/CourseTeam'; +import { CourseUpdates } from './course-updates'; /** * As of this writing, these routes are mounted at a path prefixed with the following: @@ -43,10 +44,7 @@ const CourseAuthoringRoutes = ({ courseId }) => { )} - {process.env.ENABLE_NEW_UPDATES_PAGE === 'true' - && ( - - )} + diff --git a/src/assets/scss/_animations.scss b/src/assets/scss/_animations.scss new file mode 100644 index 000000000..2100b62a4 --- /dev/null +++ b/src/assets/scss/_animations.scss @@ -0,0 +1,9 @@ +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/src/constants.js b/src/constants.js index e0d0481bb..a8c3742e1 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,7 +1,7 @@ export const DATE_FORMAT = 'MM/dd/yyyy'; export const TIME_FORMAT = 'HH:mm'; export const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss\\Z'; -export const FORMATTED_DATE_FORMAT = 'MMMM D, YYYY'; +export const COMMA_SEPARATED_DATE_FORMAT = 'MMMM D, YYYY'; export const DEFAULT_EMPTY_WYSIWYG_VALUE = '

 

'; export const STATEFUL_BUTTON_STATES = { pending: 'pending', @@ -17,3 +17,9 @@ export const BADGE_STATES = { danger: 'danger', secondary: 'secondary', }; + +export const NOTIFICATION_MESSAGES = { + saving: 'Saving', + duplicating: 'Duplicating', + deleting: 'Deleting', +}; diff --git a/src/course-updates/CourseUpdates.jsx b/src/course-updates/CourseUpdates.jsx new file mode 100644 index 000000000..cfcdfea96 --- /dev/null +++ b/src/course-updates/CourseUpdates.jsx @@ -0,0 +1,163 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Button, + Container, + Layout, +} from '@edx/paragon'; +import { Add as AddIcon } from '@edx/paragon/icons'; +import { useSelector } from 'react-redux'; + +import { getProcessingNotification } from '../generic/processing-notification/data/selectors'; +import ProcessingNotification from '../generic/processing-notification'; +import SubHeader from '../generic/sub-header/SubHeader'; +import InternetConnectionAlert from '../generic/internet-connection-alert'; +import { RequestStatus } from '../data/constants'; +import CourseHandouts from './course-handouts/CourseHandouts'; +import CourseUpdate from './course-update/CourseUpdate'; +import DeleteModal from './delete-modal/DeleteModal'; +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 { matchesAnyStatus } from './utils'; + +const CourseUpdates = ({ courseId }) => { + const intl = useIntl(); + + const { + requestType, + courseUpdates, + courseHandouts, + courseUpdatesInitialValues, + isMainFormOpen, + isInnerFormOpen, + isUpdateFormOpen, + isDeleteModalOpen, + closeUpdateForm, + closeDeleteModal, + handleUpdatesSubmit, + handleOpenUpdateForm, + handleOpenDeleteForm, + handleDeleteUpdateSubmit, + } = useCourseUpdates({ courseId }); + + const { + isShow: isShowProcessingNotification, + title: processingNotificationTitle, + } = useSelector(getProcessingNotification); + + const loadingStatuses = useSelector(getLoadingStatuses); + const savingStatuses = useSelector(getSavingStatuses); + + const anyStatusFailed = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.FAILED); + const anyStatusInProgress = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.IN_PROGRESS); + const anyStatusPending = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.PENDING); + + return ( + <> + +
+ + +
+
+ handleOpenUpdateForm(REQUEST_TYPES.add_new_update)} + disabled={isUpdateFormOpen} + > + {intl.formatMessage(messages.newUpdateButton)} + + )} + /> +
+ {isMainFormOpen && ( + + )} +
+
+ {courseUpdates.length ? courseUpdates.map((courseUpdate, index) => ( + isInnerFormOpen(courseUpdate.id) ? ( + + ) : ( + handleOpenUpdateForm(REQUEST_TYPES.edit_update, courseUpdate)} + onDelete={() => handleOpenDeleteForm(courseUpdate)} + isDisabledButtons={isUpdateFormOpen} + /> + ))) : null} +
+
+ handleOpenUpdateForm(REQUEST_TYPES.edit_handouts)} + isDisabledButtons={isUpdateFormOpen} + /> +
+ + {isShowProcessingNotification && ( + + )} +
+
+
+
+
+
+
+
+
+ null} + /> +
+ + ); +}; + +CourseUpdates.propTypes = { + courseId: PropTypes.string.isRequired, +}; + +export default CourseUpdates; diff --git a/src/course-updates/CourseUpdates.scss b/src/course-updates/CourseUpdates.scss new file mode 100644 index 000000000..72a9ff7ab --- /dev/null +++ b/src/course-updates/CourseUpdates.scss @@ -0,0 +1,20 @@ +@import "./course-handouts/CourseHandouts"; +@import "./course-update/CourseUpdate"; +@import "./update-form/UpdateForm"; + +.updates-container { + @include pgn-box-shadow(1, "centered"); + + display: grid; + grid-template-columns: 65% 35%; + border: .0625rem solid $gray-200; + border-radius: .375rem; + background: $white; + overflow: hidden; +} + +.updates-handouts-container { + border-left: .0625rem solid $gray-200; + padding: 1.875rem; + background: $white; +} diff --git a/src/course-updates/CourseUpdates.test.jsx b/src/course-updates/CourseUpdates.test.jsx new file mode 100644 index 000000000..46bdff9aa --- /dev/null +++ b/src/course-updates/CourseUpdates.test.jsx @@ -0,0 +1,220 @@ +import React from 'react'; +import { render, waitFor, fireEvent } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { + getCourseUpdatesApiUrl, + getCourseHandoutApiUrl, + updateCourseUpdatesApiUrl, +} from './data/api'; +import { + createCourseUpdateQuery, + deleteCourseUpdateQuery, + editCourseHandoutsQuery, + editCourseUpdateQuery, +} from './data/thunk'; +import initializeStore from '../store'; +import { executeThunk } from '../utils'; +import { courseUpdatesMock, courseHandoutsMock } from './__mocks__'; +import CourseUpdates from './CourseUpdates'; +import messages from './messages'; + +let axiosMock; +let store; +const mockPathname = '/foo-bar'; +const courseId = '123'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: mockPathname, + }), +})); + +jest.mock('@tinymce/tinymce-react', () => { + const originalModule = jest.requireActual('@tinymce/tinymce-react'); + return { + __esModule: true, + ...originalModule, + Editor: () => 'foo bar', + }; +}); + +jest.mock('@edx/frontend-lib-content-components', () => ({ + TinyMceWidget: () =>
Widget
, + prepareEditorRef: jest.fn(() => ({ + refReady: true, + setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'), + })), +})); + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + 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('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(); + }); + }); + + it('should create course update', async () => { + const { getByText } = render(); + + const data = { + content: '

Some text

', + date: 'August 29, 2023', + }; + + 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, getAllByRole } = render(); + + await waitFor(() => { + const editButtons = getAllByRole('button', { name: 'Edit' }); + const deleteButtons = getAllByRole('button', { name: 'Delete' }); + const newUpdateButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage }); + + fireEvent.click(newUpdateButton); + + expect(newUpdateButton).toBeDisabled(); + editButtons.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, getByTestId, getAllByRole, + } = render(); + + await waitFor(() => { + const editHandoutsButton = getByTestId('course-handouts-edit-button'); + const editButtons = getAllByRole('button', { name: 'Edit' }); + const deleteButtons = getAllByRole('button', { name: 'Delete' }); + + fireEvent.click(editHandoutsButton); + + expect(editHandoutsButton).toBeDisabled(); + expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled(); + editButtons.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, getAllByRole, queryByText, + } = render(); + + await waitFor(() => { + const editUpdateFirstButton = getAllByTestId('course-update-edit-button')[0]; + const editButtons = getAllByRole('button', { name: 'Edit' }); + const deleteButtons = getAllByRole('button', { name: 'Delete' }); + + fireEvent.click(editUpdateFirstButton); + expect(getByText('Edit update')).toBeInTheDocument(); + expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled(); + editButtons.forEach((button) => expect(button).toBeDisabled()); + deleteButtons.forEach((button) => expect(button).toBeDisabled()); + expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/course-updates/__mocks__/courseHandouts.js b/src/course-updates/__mocks__/courseHandouts.js new file mode 100644 index 000000000..55a6b2579 --- /dev/null +++ b/src/course-updates/__mocks__/courseHandouts.js @@ -0,0 +1,83 @@ +module.exports = { + id: 'block-v1:edX+DemoX+Demo_Course+type@course_info+block@handouts', + display_name: 'Text', + category: 'course_info', + has_children: false, + edited_on: 'Jul 12, 2023 at 17:52 UTC', + published: true, + published_on: 'Jul 12, 2023 at 17:52 UTC', + studio_url: null, + released_to_students: false, + release_date: null, + visibility_state: 'unscheduled', + has_explicit_staff_lock: false, + start: '2030-01-01T00:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: null, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + data: 'Some handouts', + metadata: {}, + ancestor_has_staff_lock: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, +}; diff --git a/src/course-updates/__mocks__/courseUpdates.js b/src/course-updates/__mocks__/courseUpdates.js new file mode 100644 index 000000000..dca2909eb --- /dev/null +++ b/src/course-updates/__mocks__/courseUpdates.js @@ -0,0 +1,5 @@ +module.exports = [ + { id: 1, date: 'July 11, 2023', content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' }, + { id: 2, date: 'August 20, 2023', content: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.' }, + { id: 3, date: 'January 30, 2023', content: 'But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself' }, +]; diff --git a/src/course-updates/__mocks__/index.js b/src/course-updates/__mocks__/index.js new file mode 100644 index 000000000..bb4cba111 --- /dev/null +++ b/src/course-updates/__mocks__/index.js @@ -0,0 +1,2 @@ +export { default as courseUpdatesMock } from './courseUpdates'; +export { default as courseHandoutsMock } from './courseHandouts'; diff --git a/src/course-updates/constants.js b/src/course-updates/constants.js new file mode 100644 index 000000000..fd082b7d5 --- /dev/null +++ b/src/course-updates/constants.js @@ -0,0 +1,7 @@ +// eslint-disable-next-line import/prefer-default-export +export const REQUEST_TYPES = { + add_new_update: 'add_new_update', + edit_update: 'edit_update', + edit_handouts: 'edit_handouts', + delete_update: 'delete_update', +}; diff --git a/src/course-updates/course-handouts/CourseHandouts.jsx b/src/course-updates/course-handouts/CourseHandouts.jsx new file mode 100644 index 000000000..ef374a01e --- /dev/null +++ b/src/course-updates/course-handouts/CourseHandouts.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; + +const CourseHandouts = ({ contentForHandouts, onEdit, isDisabledButtons }) => { + const intl = useIntl(); + + return ( +
+
+

{intl.formatMessage(messages.handoutsTitle)}

+ +
+
+
+ ); +}; + +CourseHandouts.propTypes = { + contentForHandouts: PropTypes.string.isRequired, + onEdit: PropTypes.func.isRequired, + isDisabledButtons: PropTypes.bool.isRequired, +}; + +export default CourseHandouts; diff --git a/src/course-updates/course-handouts/CourseHandouts.scss b/src/course-updates/course-handouts/CourseHandouts.scss new file mode 100644 index 000000000..731ddae5d --- /dev/null +++ b/src/course-updates/course-handouts/CourseHandouts.scss @@ -0,0 +1,16 @@ +.course-handouts { + .course-handouts-header { + display: flex; + justify-content: space-between; + margin-bottom: $spacer; + + .course-handouts-header__title { + font-weight: 300; + color: $gray-800; + } + + .course-handouts-header__btn { + align-self: flex-start; + } + } +} diff --git a/src/course-updates/course-handouts/CourseHandouts.test.jsx b/src/course-updates/course-handouts/CourseHandouts.test.jsx new file mode 100644 index 000000000..673244227 --- /dev/null +++ b/src/course-updates/course-handouts/CourseHandouts.test.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import CourseHandouts from './CourseHandouts'; +import messages from './messages'; + +const onEditMock = jest.fn(); +const handoutsContentMock = 'Handouts Content'; + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('render CourseHandouts component correctly', () => { + const { getByText, getByRole } = renderComponent(); + + expect(getByText(messages.handoutsTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(handoutsContentMock)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeInTheDocument(); + }); + + it('calls the onEdit function when the edit button is clicked', () => { + const { getByRole } = renderComponent(); + + const editButton = getByRole('button', { name: messages.editButton.defaultMessage }); + fireEvent.click(editButton); + expect(onEditMock).toHaveBeenCalledTimes(1); + }); + + it('"Edit" button is disabled when isDisabledButtons is true', () => { + const { getByRole } = renderComponent({ isDisabledButtons: true }); + + const editButton = getByRole('button', { name: messages.editButton.defaultMessage }); + expect(editButton).toBeDisabled(); + }); +}); diff --git a/src/course-updates/course-handouts/messages.js b/src/course-updates/course-handouts/messages.js new file mode 100644 index 000000000..ea412c6a4 --- /dev/null +++ b/src/course-updates/course-handouts/messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + handoutsTitle: { + id: 'course-authoring.course-updates.handouts.title', + defaultMessage: 'Course handouts', + }, + editButton: { + id: 'course-authoring.course-updates.actions.edit', + defaultMessage: 'Edit', + }, +}); + +export default messages; diff --git a/src/course-updates/course-update/CourseUpdate.jsx b/src/course-updates/course-update/CourseUpdate.jsx new file mode 100644 index 000000000..dffc3d583 --- /dev/null +++ b/src/course-updates/course-update/CourseUpdate.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Icon } from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Error as ErrorIcon } from '@edx/paragon/icons/es5'; + +import { isDateForUpdateValid } from './utils'; +import messages from './messages'; + +const CourseUpdate = ({ + dateForUpdate, + contentForUpdate, + onEdit, + onDelete, + isDisabledButtons, +}) => { + const intl = useIntl(); + + return ( +
+
+ {dateForUpdate} + {!isDateForUpdateValid(dateForUpdate) && ( +
+ +

{intl.formatMessage(messages.errorMessage)}

+
+ )} +
+ + +
+
+ {Boolean(contentForUpdate) && ( +
+ )} +
+ ); +}; + +CourseUpdate.propTypes = { + dateForUpdate: PropTypes.string.isRequired, + contentForUpdate: PropTypes.string.isRequired, + onEdit: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + isDisabledButtons: PropTypes.bool.isRequired, +}; + +export default CourseUpdate; diff --git a/src/course-updates/course-update/CourseUpdate.scss b/src/course-updates/course-update/CourseUpdate.scss new file mode 100644 index 000000000..43f98bdfa --- /dev/null +++ b/src/course-updates/course-update/CourseUpdate.scss @@ -0,0 +1,36 @@ +.course-update { + &:not(:first-child) { + padding-top: 1.875rem; + margin-top: 1.875rem; + border-top: 1px solid $light-400; + } + + .course-update-header { + display: flex; + align-items: center; + margin-bottom: 1.125rem; + gap: .5rem; + + .course-update-header__date { + line-height: 1.875rem; + letter-spacing: 1px; + } + + .course-update-header__error { + display: flex; + align-items: center; + gap: .25rem; + + svg { + color: $warning-300; + } + } + + .course-update-header__action { + display: flex; + width: auto; + margin-left: auto; + gap: .5rem; + } + } +} diff --git a/src/course-updates/course-update/CourseUpdate.test.jsx b/src/course-updates/course-update/CourseUpdate.test.jsx new file mode 100644 index 000000000..32e444729 --- /dev/null +++ b/src/course-updates/course-update/CourseUpdate.test.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import CourseUpdate from './CourseUpdate'; +import messages from './messages'; + +const onEditMock = jest.fn(); +const onDeleteMock = jest.fn(); +const dateForUpdateMock = 'May 1, 2023'; +const contentForUpdateMock = 'Update Content'; + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('render CourseUpdate component correctly', () => { + const { getByText, getByRole } = renderComponent(); + + expect(getByText(dateForUpdateMock)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.deleteButton.defaultMessage })).toBeInTheDocument(); + }); + + it('render CourseUpdate component without content correctly', () => { + const { getByText, queryByTestId, getByRole } = renderComponent({ contentForUpdate: '' }); + + expect(getByText(dateForUpdateMock)).toBeInTheDocument(); + expect(queryByTestId('course-update-content')).not.toBeInTheDocument(); + expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.deleteButton.defaultMessage })).toBeInTheDocument(); + }); + + it('render error message when dateForUpdate is inValid', () => { + const { getByText } = renderComponent({ dateForUpdate: 'Welcome' }); + + expect(getByText(messages.errorMessage.defaultMessage)).toBeInTheDocument(); + }); + + it('calls the onEdit function when the "Edit" button is clicked', () => { + const { getByRole } = renderComponent(); + + const editButton = getByRole('button', { name: messages.editButton.defaultMessage }); + fireEvent.click(editButton); + expect(onEditMock).toHaveBeenCalledTimes(1); + }); + + it('calls the onDelete function when the "Delete" button is clicked', () => { + const { getByRole } = renderComponent(); + + const deleteButton = getByRole('button', { name: messages.deleteButton.defaultMessage }); + fireEvent.click(deleteButton); + expect(onDeleteMock).toHaveBeenCalledTimes(1); + }); + + it('"Edit" and "Delete" buttons is disabled when isDisabledButtons is true', () => { + const { getByRole } = renderComponent({ isDisabledButtons: true }); + + expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeDisabled(); + expect(getByRole('button', { name: messages.deleteButton.defaultMessage })).toBeDisabled(); + }); +}); diff --git a/src/course-updates/course-update/messages.js b/src/course-updates/course-update/messages.js new file mode 100644 index 000000000..0814df91d --- /dev/null +++ b/src/course-updates/course-update/messages.js @@ -0,0 +1,18 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + editButton: { + id: 'course-authoring.course-updates.button.edit', + defaultMessage: 'Edit', + }, + deleteButton: { + id: 'course-authoring.course-updates.button.delete', + defaultMessage: 'Delete', + }, + errorMessage: { + id: 'course-authoring.course-updates.date-invalid', + defaultMessage: 'Action required: Enter a valid date.', + }, +}); + +export default messages; diff --git a/src/course-updates/course-update/utils.js b/src/course-updates/course-update/utils.js new file mode 100644 index 000000000..a063c1962 --- /dev/null +++ b/src/course-updates/course-update/utils.js @@ -0,0 +1,17 @@ +import moment from 'moment'; + +import { COMMA_SEPARATED_DATE_FORMAT } from '../../constants'; + +/** + * Check is valid date format in course update + * @param {string} date - date for update + * @returns {boolean} - is valid date format + */ +const isDateForUpdateValid = (date) => { + const parsedDate = moment(date, COMMA_SEPARATED_DATE_FORMAT, true); + + return parsedDate.isValid() && parsedDate.format(COMMA_SEPARATED_DATE_FORMAT) === date; +}; + +// eslint-disable-next-line import/prefer-default-export +export { isDateForUpdateValid }; diff --git a/src/course-updates/data/api.js b/src/course-updates/data/api.js new file mode 100644 index 000000000..818ccd1ed --- /dev/null +++ b/src/course-updates/data/api.js @@ -0,0 +1,84 @@ +import { getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; +export const getCourseUpdatesApiUrl = (courseId) => `${getApiBaseUrl()}/course_info_update/${courseId}/`; +export const updateCourseUpdatesApiUrl = (courseId, updateId) => `${getApiBaseUrl()}/course_info_update/${courseId}/${updateId}`; +export const getCourseHandoutApiUrl = (courseId) => { + const formattedCourseId = courseId.split('course-v1:')[1]; + return `${getApiBaseUrl()}/xblock/block-v1:${formattedCourseId}+type@course_info+block@handouts`; +}; + +/** + * Get course updates. + * @param {string} courseId + * @returns {Promise} + */ +export async function getCourseUpdates(courseId) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseUpdatesApiUrl(courseId)); + + return data; +} + +/** + * Create new course update. + * @param {string} courseId + * @param {object} courseUpdate + * @returns {Promise} + */ +export async function createUpdate(courseId, courseUpdate) { + const { data } = await getAuthenticatedHttpClient() + .post(getCourseUpdatesApiUrl(courseId), courseUpdate); + + return data; +} + +/** + * Edit course update. + * @param {string} courseId + * @param {object} courseUpdate + * @returns {Promise} + */ +export async function editUpdate(courseId, courseUpdate) { + const { data } = await getAuthenticatedHttpClient() + .put(updateCourseUpdatesApiUrl(courseId, courseUpdate.id), courseUpdate); + + return data; +} + +/** + * Delete course update. + * @param {string} courseId + * @param {number} updateId +1 */ +export async function deleteUpdate(courseId, updateId) { + const { data } = await getAuthenticatedHttpClient() + .delete(updateCourseUpdatesApiUrl(courseId, updateId)); + + return data; +} + +/** + * Get course handouts. + * @param {string} courseId + * @returns {Promise} + */ +export async function getCourseHandouts(courseId) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseHandoutApiUrl(courseId)); + return data; +} + +/** + * Edit course handouts. + * @param {string} courseId + * @param {object} courseHandouts + * @returns {Promise} + */ +export async function editHandouts(courseId, courseHandouts) { + const { data } = await getAuthenticatedHttpClient() + .put(getCourseHandoutApiUrl(courseId), courseHandouts); + + return data; +} diff --git a/src/course-updates/data/selectors.js b/src/course-updates/data/selectors.js new file mode 100644 index 000000000..947ad0f8a --- /dev/null +++ b/src/course-updates/data/selectors.js @@ -0,0 +1,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; diff --git a/src/course-updates/data/slice.js b/src/course-updates/data/slice.js new file mode 100644 index 000000000..18cd86a1a --- /dev/null +++ b/src/course-updates/data/slice.js @@ -0,0 +1,72 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; +import { sortBy } from 'lodash'; + +const initialState = { + courseUpdates: [], + courseHandouts: {}, + savingStatuses: { + createCourseUpdateQuery: '', + editCourseUpdateQuery: '', + deleteCourseUpdateQuery: '', + editCourseHandoutsQuery: '', + }, + loadingStatuses: { + fetchCourseUpdatesQuery: '', + fetchCourseHandoutsQuery: '', + }, +}; + +const slice = createSlice({ + name: 'courseUpdates', + initialState, + reducers: { + fetchCourseUpdatesSuccess: (state, { payload }) => { + state.courseUpdates = payload; + }, + createCourseUpdate: (state, { payload }) => { + state.courseUpdates = [payload, ...state.courseUpdates]; + }, + editCourseUpdate: (state, { payload }) => { + state.courseUpdates = state.courseUpdates.map((courseUpdate) => { + if (courseUpdate.id === payload.id) { + return payload; + } + return courseUpdate; + }); + }, + deleteCourseUpdate: (state, { payload }) => { + state.courseUpdates = sortBy(payload, 'id').reverse(); + }, + fetchCourseHandoutsSuccess: (state, { payload }) => { + state.courseHandouts = payload; + }, + editCourseHandouts: (state, { payload }) => { + state.courseHandouts = { + ...state.courseHandouts, + ...payload, + }; + }, + updateSavingStatuses: (state, { payload }) => { + state.savingStatuses = { ...state.savingStatuses, ...payload }; + }, + updateLoadingStatuses: (state, { payload }) => { + state.loadingStatuses = { ...state.loadingStatuses, ...payload }; + }, + }, +}); + +export const { + fetchCourseUpdatesSuccess, + createCourseUpdate, + editCourseUpdate, + deleteCourseUpdate, + fetchCourseHandoutsSuccess, + editCourseHandouts, + updateSavingStatuses, + updateLoadingStatuses, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/course-updates/data/thunk.js b/src/course-updates/data/thunk.js new file mode 100644 index 000000000..8713b808c --- /dev/null +++ b/src/course-updates/data/thunk.js @@ -0,0 +1,111 @@ +import { NOTIFICATION_MESSAGES } from '../../constants'; +import { RequestStatus } from '../../data/constants'; +import { hideProcessingNotification, showProcessingNotification } from '../../generic/processing-notification/data/slice'; +import { + getCourseUpdates, + getCourseHandouts, + createUpdate, + editUpdate, + deleteUpdate, + editHandouts, +} from './api'; +import { + fetchCourseUpdatesSuccess, + createCourseUpdate, + editCourseUpdate, + deleteCourseUpdate, + fetchCourseHandoutsSuccess, + editCourseHandouts, + updateLoadingStatuses, + updateSavingStatuses, +} from './slice'; + +export function fetchCourseUpdatesQuery(courseId) { + return async (dispatch) => { + try { + dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.IN_PROGRESS })); + const courseUpdates = await getCourseUpdates(courseId); + dispatch(fetchCourseUpdatesSuccess(courseUpdates)); + dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.FAILED })); + } + }; +} + +export function createCourseUpdateQuery(courseId, data) { + return async (dispatch) => { + try { + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving)); + const courseUpdate = await createUpdate(courseId, data); + dispatch(createCourseUpdate(courseUpdate)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + } + }; +} + +export function editCourseUpdateQuery(courseId, data) { + return async (dispatch) => { + try { + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving)); + const courseUpdate = await editUpdate(courseId, data); + dispatch(editCourseUpdate(courseUpdate)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + } + }; +} + +export function deleteCourseUpdateQuery(courseId, updateId) { + return async (dispatch) => { + try { + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.deleting)); + const courseUpdates = await deleteUpdate(courseId, updateId); + dispatch(deleteCourseUpdate(courseUpdates)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + } + }; +} + +export function fetchCourseHandoutsQuery(courseId) { + return async (dispatch) => { + try { + dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.IN_PROGRESS })); + const courseHandouts = await getCourseHandouts(courseId); + dispatch(fetchCourseHandoutsSuccess(courseHandouts)); + dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(updateLoadingStatuses({ fetchCourseHandoutsQuery: RequestStatus.FAILED })); + } + }; +} + +export function editCourseHandoutsQuery(courseId, data) { + return async (dispatch) => { + try { + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving)); + const courseHandouts = await editHandouts(courseId, data); + dispatch(editCourseHandouts(courseHandouts)); + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(hideProcessingNotification()); + dispatch(updateSavingStatuses({ createCourseUpdateQuery: RequestStatus.FAILED })); + } + }; +} diff --git a/src/course-updates/delete-modal/DeleteModal.jsx b/src/course-updates/delete-modal/DeleteModal.jsx new file mode 100644 index 000000000..f3ea1608c --- /dev/null +++ b/src/course-updates/delete-modal/DeleteModal.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + ActionRow, + Button, + AlertModal, +} from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; + +const DeleteModal = ({ isOpen, close, onDeleteSubmit }) => { + const intl = useIntl(); + + return ( + + + + + )} + > +

{intl.formatMessage(messages.deleteModalDescription)}

+
+ ); +}; + +DeleteModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + onDeleteSubmit: PropTypes.func.isRequired, +}; + +export default DeleteModal; diff --git a/src/course-updates/delete-modal/DeleteModal.test.jsx b/src/course-updates/delete-modal/DeleteModal.test.jsx new file mode 100644 index 000000000..bfd54e0bb --- /dev/null +++ b/src/course-updates/delete-modal/DeleteModal.test.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import DeleteModal from './DeleteModal'; +import messages from './messages'; + +const onDeleteSubmitMock = jest.fn(); +const closeMock = jest.fn(); + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('render DeleteModal component correctly', () => { + const { getByText, getByRole } = renderComponent(); + + expect(getByText(messages.deleteModalTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.deleteModalDescription.defaultMessage)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.okButton.defaultMessage })).toBeInTheDocument(); + }); + + it('calls onDeleteSubmit function when the "Ok" button is clicked', () => { + const { getByRole } = renderComponent(); + + const okButton = getByRole('button', { name: messages.okButton.defaultMessage }); + fireEvent.click(okButton); + expect(onDeleteSubmitMock).toHaveBeenCalledTimes(1); + }); + + it('calls the close function when the "Cancel" button is clicked', () => { + const { getByRole } = renderComponent(); + + const cancelButton = getByRole('button', { name: messages.cancelButton.defaultMessage }); + fireEvent.click(cancelButton); + expect(closeMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/course-updates/delete-modal/messages.js b/src/course-updates/delete-modal/messages.js new file mode 100644 index 000000000..c8100d05b --- /dev/null +++ b/src/course-updates/delete-modal/messages.js @@ -0,0 +1,22 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + deleteModalTitle: { + id: 'course-authoring.course-updates.delete-modal.title', + defaultMessage: 'Are you sure you want to delete this update?', + }, + deleteModalDescription: { + id: 'course-authoring.course-updates.delete-modal.description', + defaultMessage: 'This action cannot be undone.', + }, + cancelButton: { + id: 'course-authoring.course-updates.actions.cancel', + defaultMessage: 'Cancel', + }, + okButton: { + id: 'course-authoring.course-updates.button.ok', + defaultMessage: 'Ok', + }, +}); + +export default messages; diff --git a/src/course-updates/hooks.jsx b/src/course-updates/hooks.jsx new file mode 100644 index 000000000..20c9c2947 --- /dev/null +++ b/src/course-updates/hooks.jsx @@ -0,0 +1,114 @@ +import { useDispatch, useSelector } from 'react-redux'; +import moment from 'moment/moment'; +import { useEffect, useState } from 'react'; +import { useToggle } from '@edx/paragon'; + +import { COMMA_SEPARATED_DATE_FORMAT } from '../constants'; +import { getCourseHandouts, getCourseUpdates } from './data/selectors'; +import { REQUEST_TYPES } from './constants'; +import { + createCourseUpdateQuery, + deleteCourseUpdateQuery, + editCourseHandoutsQuery, + editCourseUpdateQuery, + fetchCourseHandoutsQuery, + fetchCourseUpdatesQuery, +} from './data/thunk'; + +const useCourseUpdates = ({ courseId }) => { + const dispatch = useDispatch(); + const initialUpdate = { id: 0, date: moment().utc().toDate(), content: '' }; + + const [requestType, setRequestType] = useState(''); + const [isUpdateFormOpen, openUpdateForm, closeUpdateForm] = useToggle(false); + const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); + const [currentUpdate, setCurrentUpdate] = useState(initialUpdate); + + const courseUpdates = useSelector(getCourseUpdates); + const courseHandouts = useSelector(getCourseHandouts); + + const courseUpdatesInitialValues = requestType === REQUEST_TYPES.edit_handouts + ? courseHandouts + : currentUpdate; + + const handleOpenUpdateForm = (type, courseUpdate) => { + setRequestType(type); + + switch (type) { + case REQUEST_TYPES.add_new_update: + setCurrentUpdate(initialUpdate); + break; + case REQUEST_TYPES.edit_update: + setCurrentUpdate(courseUpdate); + break; + default: + window.scrollTo(0, 0); + } + + openUpdateForm(); + }; + + const handleOpenDeleteForm = (courseUpdate) => { + setRequestType(REQUEST_TYPES.delete_update); + setCurrentUpdate(courseUpdate); + openDeleteModal(); + }; + + const handleUpdatesSubmit = (data) => { + const dataToSend = { + ...data, + date: moment(data.date).format(COMMA_SEPARATED_DATE_FORMAT), + }; + const { id, date, content } = dataToSend; + + const handleQuerySubmit = (handler) => { + closeUpdateForm(); + setCurrentUpdate(initialUpdate); + return handler(); + }; + + switch (requestType) { + case REQUEST_TYPES.add_new_update: + return handleQuerySubmit(dispatch(createCourseUpdateQuery(courseId, { date, content }))); + case REQUEST_TYPES.edit_update: + return handleQuerySubmit(dispatch(editCourseUpdateQuery(courseId, { id, date, content }))); + case REQUEST_TYPES.edit_handouts: + return handleQuerySubmit(dispatch(editCourseHandoutsQuery(courseId, { ...data, data: data?.data || '' }))); + default: + return true; + } + }; + + const handleDeleteUpdateSubmit = () => { + const { id } = currentUpdate; + + dispatch(deleteCourseUpdateQuery(courseId, id)); + setCurrentUpdate(initialUpdate); + closeDeleteModal(); + }; + + useEffect(() => { + dispatch(fetchCourseUpdatesQuery(courseId)); + dispatch(fetchCourseHandoutsQuery(courseId)); + }, [courseId]); + + return { + requestType, + courseUpdates, + courseHandouts, + courseUpdatesInitialValues, + isMainFormOpen: isUpdateFormOpen && requestType !== REQUEST_TYPES.edit_update, + isInnerFormOpen: (id) => isUpdateFormOpen && currentUpdate.id === id && requestType === REQUEST_TYPES.edit_update, + isUpdateFormOpen, + isDeleteModalOpen, + closeUpdateForm, + closeDeleteModal, + handleUpdatesSubmit, + handleOpenUpdateForm, + handleDeleteUpdateSubmit, + handleOpenDeleteForm, + }; +}; + +// eslint-disable-next-line import/prefer-default-export +export { useCourseUpdates }; diff --git a/src/course-updates/index.js b/src/course-updates/index.js new file mode 100644 index 000000000..886e19e97 --- /dev/null +++ b/src/course-updates/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as CourseUpdates } from './CourseUpdates'; diff --git a/src/course-updates/messages.js b/src/course-updates/messages.js new file mode 100644 index 000000000..b00a9d647 --- /dev/null +++ b/src/course-updates/messages.js @@ -0,0 +1,22 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + headingTitle: { + id: 'course-authoring.course-updates.header.title', + defaultMessage: 'Course updates', + }, + headingSubtitle: { + id: 'course-authoring.course-updates.header.subtitle', + defaultMessage: 'Content', + }, + 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. You add or edit updates in HTML.', + }, + newUpdateButton: { + id: 'course-authoring.course-updates.actions.new-update', + defaultMessage: 'New update', + }, +}); + +export default messages; diff --git a/src/course-updates/update-form/UpdateForm.jsx b/src/course-updates/update-form/UpdateForm.jsx new file mode 100644 index 000000000..dec0913e0 --- /dev/null +++ b/src/course-updates/update-form/UpdateForm.jsx @@ -0,0 +1,141 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + ActionRow, + Button, + Form, + Icon, +} from '@edx/paragon'; +import classNames from 'classnames'; +import DatePicker from 'react-datepicker/dist'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Calendar as CalendarIcon, Error as ErrorIcon } from '@edx/paragon/icons'; +import { Formik } from 'formik'; + +import { + convertToStringFromDate, + convertToDateFromString, + isValidDate, +} from '../../utils'; +import { DATE_FORMAT, DEFAULT_EMPTY_WYSIWYG_VALUE } from '../../constants'; +import { WysiwygEditor } from '../../generic/WysiwygEditor'; +import { REQUEST_TYPES } from '../constants'; +import { geUpdateFormSettings } from './utils'; +import messages from './messages'; + +const UpdateForm = ({ + close, + requestType, + onSubmit, + courseUpdatesInitialValues, + isInnerForm, + isFirstUpdate, +}) => { + const intl = useIntl(); + + const { + currentContent, + formTitle, + validationSchema, + contentFieldName, + submitButtonText, + } = geUpdateFormSettings(requestType, courseUpdatesInitialValues, intl); + + return ( +
+ + {({ + values, handleSubmit, isValid, setFieldValue, + }) => ( + <> +

{formTitle}

+ {(requestType !== REQUEST_TYPES.edit_handouts) && ( + + + {intl.formatMessage(messages.updateFormDate)} + +
+ + { + if (!isValidDate(value)) { + return; + } + setFieldValue('date', convertToStringFromDate(value)); + }} + /> +
+ {!isValid && ( +
+ + {intl.formatMessage(messages.updateFormInValid)} +
+ )} +
+ )} + + { + setFieldValue(contentFieldName, value || DEFAULT_EMPTY_WYSIWYG_VALUE); + }} + /> + + + + + + + )} +
+
+ ); +}; + +UpdateForm.defaultProps = { + isInnerForm: false, + isFirstUpdate: false, +}; + +UpdateForm.propTypes = { + // eslint-disable-next-line react/forbid-prop-types + courseUpdatesInitialValues: PropTypes.object.isRequired, + close: PropTypes.func.isRequired, + requestType: PropTypes.string.isRequired, + onSubmit: PropTypes.func.isRequired, + isInnerForm: PropTypes.bool, + isFirstUpdate: PropTypes.bool, +}; + +export default UpdateForm; diff --git a/src/course-updates/update-form/UpdateForm.scss b/src/course-updates/update-form/UpdateForm.scss new file mode 100644 index 000000000..82aed3feb --- /dev/null +++ b/src/course-updates/update-form/UpdateForm.scss @@ -0,0 +1,63 @@ +.update-form { + @include pgn-box-shadow(1, "centered"); + + border: .0625rem solid $gray-200; + border-radius: .375rem; + background: $white; + margin-bottom: map-get($spacers, 4); + padding: $spacer 1.875rem; + + .update-form-title { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: $spacer; + } + + .datepicker-field { + display: flex; + align-items: center; + gap: .5rem; + position: relative; + + .datepicker-float-labels { + position: absolute; + padding: 0 .1875rem; + top: -.625rem; + left: .3125rem; + z-index: 9; + background-color: $white; + } + + .datepicker-field-error { + display: flex; + align-items: center; + gap: .25rem; + + svg { + color: $warning-300; + } + } + + .react-datepicker-popper { + z-index: $zindex-dropdown; + } + } +} + +.update-form__inner { + margin-bottom: 0; + margin-top: 1.875rem; + padding: map-get($spacers, 4) 0 0; + border-top: .0625rem solid $light-400; + border-bottom: none; + border-left: none; + border-right: none; + border-radius: 0; + box-shadow: none; +} + +.update-form__inner-first { + border-top: none; + padding-top: 0; + margin-top: 0; +} diff --git a/src/course-updates/update-form/UpdateForm.test.jsx b/src/course-updates/update-form/UpdateForm.test.jsx new file mode 100644 index 000000000..569efb725 --- /dev/null +++ b/src/course-updates/update-form/UpdateForm.test.jsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { + render, + fireEvent, + waitFor, + act, +} from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import moment from 'moment/moment'; + +import { REQUEST_TYPES } from '../constants'; +import { courseHandoutsMock, courseUpdatesMock } from '../__mocks__'; +import UpdateForm from './UpdateForm'; +import messages from './messages'; + +const closeMock = jest.fn(); +const onSubmitMock = jest.fn(); +const addNewUpdateMock = { id: 0, date: moment().utc().toDate(), content: 'Some content' }; +const formattedDateMock = '07/11/2023'; +const contentMock = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; + +jest.mock('@tinymce/tinymce-react', () => { + const originalModule = jest.requireActual('@tinymce/tinymce-react'); + return { + __esModule: true, + ...originalModule, + Editor: () => 'foo bar', + }; +}); + +jest.mock('@edx/frontend-lib-content-components', () => ({ + TinyMceWidget: () =>
Widget
, + prepareEditorRef: jest.fn(() => ({ + refReady: true, + setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'), + })), +})); + +const courseUpdatesInitialValues = (requestType) => { + switch (requestType) { + case REQUEST_TYPES.add_new_update: + return addNewUpdateMock; + case REQUEST_TYPES.edit_update: + return courseUpdatesMock[0]; + default: + return courseHandoutsMock; + } +}; + +const renderComponent = ({ requestType }) => render( + + + , +); + +describe('', () => { + it('render Add new update form correctly', async () => { + const { getByText, getByDisplayValue, getByRole } = renderComponent({ requestType: REQUEST_TYPES.add_new_update }); + const { date } = courseUpdatesInitialValues(REQUEST_TYPES.add_new_update); + const formattedDate = moment(date).utc().format('MM/DD/yyyy'); + + expect(getByText(messages.addNewUpdateTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.updateFormDate.defaultMessage)).toBeInTheDocument(); + expect(getByDisplayValue(formattedDate)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.cancelButton.defaultMessage })); + expect(getByRole('button', { name: messages.postButton.defaultMessage })); + }); + + it('render Edit update form correctly', async () => { + const { getByText, getByDisplayValue, getByRole } = renderComponent({ requestType: REQUEST_TYPES.edit_update }); + + expect(getByText(messages.editUpdateTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.updateFormDate.defaultMessage)).toBeInTheDocument(); + expect(getByDisplayValue(formattedDateMock)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.cancelButton.defaultMessage })); + expect(getByRole('button', { name: messages.postButton.defaultMessage })); + }); + + it('render Edit handouts form correctly', async () => { + const { + getByText, getByRole, queryByTestId, queryByText, + } = renderComponent({ requestType: REQUEST_TYPES.edit_handouts }); + + expect(getByText(messages.editHandoutsTitle.defaultMessage)).toBeInTheDocument(); + expect(queryByText(messages.updateFormDate.defaultMessage)).not.toBeInTheDocument(); + expect(queryByTestId('course-updates-datepicker')).not.toBeInTheDocument(); + expect(getByRole('button', { name: messages.cancelButton.defaultMessage })); + expect(getByRole('button', { name: messages.saveButton.defaultMessage })); + }); + + it('calls closeMock when clicking cancel button', () => { + const { getByRole } = renderComponent({ requestType: REQUEST_TYPES.add_new_update }); + + const cancelButton = getByRole('button', { name: messages.cancelButton.defaultMessage }); + fireEvent.click(cancelButton); + expect(closeMock).toHaveBeenCalledTimes(1); + }); + + it('calls onSubmitMock with correct values when clicking post button', async () => { + const { getByDisplayValue, getByRole } = renderComponent({ requestType: REQUEST_TYPES.edit_update }); + const datePicker = getByDisplayValue(formattedDateMock); + const postButton = getByRole('button', { name: messages.postButton.defaultMessage }); + + fireEvent.change(datePicker, { target: { value: formattedDateMock } }); + + await act(async () => { + fireEvent.click(postButton); + }); + + await waitFor(() => { + expect(onSubmitMock).toHaveBeenCalledTimes(1); + expect(onSubmitMock).toHaveBeenCalledWith( + { + id: 1, + date: 'July 11, 2023', + content: contentMock, + }, + expect.objectContaining({ submitForm: expect.any(Function) }), + ); + }); + }); + + it('render error message when date is inValid', async () => { + const { getByDisplayValue, getByText, getByRole } = renderComponent({ requestType: REQUEST_TYPES.edit_update }); + const datePicker = getByDisplayValue(formattedDateMock); + + fireEvent.change(datePicker, { target: { value: '' } }); + + await waitFor(() => { + expect(getByText(messages.updateFormInValid.defaultMessage)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.postButton.defaultMessage })).toBeDisabled(); + }); + }); +}); diff --git a/src/course-updates/update-form/messages.js b/src/course-updates/update-form/messages.js new file mode 100644 index 000000000..8f36a9ff0 --- /dev/null +++ b/src/course-updates/update-form/messages.js @@ -0,0 +1,46 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + updateFormDate: { + id: 'course-authoring.course-updates.update-form.date', + defaultMessage: 'Date', + }, + updateFormInValid: { + id: 'course-authoring.course-updates.update-form.inValid', + defaultMessage: 'Action required: Enter a valid date.', + }, + updateFormCalendarAltText: { + id: 'course-authoring.course-updates.update-form.calendar-alt-text', + defaultMessage: 'Calendar for datepicker input', + }, + updateFormErrorAltText: { + id: 'course-authoring.course-updates.update-form.error-alt-text', + defaultMessage: 'Error icon', + }, + addNewUpdateTitle: { + id: 'course-authoring.course-updates.update-form.new-update-title', + defaultMessage: 'Add new update', + }, + editUpdateTitle: { + id: 'course-authoring.course-updates.update-form.edit-update-title', + defaultMessage: 'Edit update', + }, + editHandoutsTitle: { + id: 'course-authoring.course-updates.update-form.edit-handouts-title', + defaultMessage: 'Edit handouts', + }, + saveButton: { + id: 'course-authoring.course-updates.actions.save', + defaultMessage: 'Save', + }, + postButton: { + id: 'course-authoring.course-updates.actions.post', + defaultMessage: 'Post', + }, + cancelButton: { + id: 'course-authoring.course-updates.actions.cancel', + defaultMessage: 'Cancel', + }, +}); + +export default messages; diff --git a/src/course-updates/update-form/utils.js b/src/course-updates/update-form/utils.js new file mode 100644 index 000000000..7f6ce9d05 --- /dev/null +++ b/src/course-updates/update-form/utils.js @@ -0,0 +1,56 @@ +import * as Yup from 'yup'; + +import { REQUEST_TYPES } from '../constants'; +import messages from './messages'; + +/** + * Get Update form settings depending on requestType + * @param {typeof REQUEST_TYPES} requestType - one of REQUEST_TYPES + * @param {object} courseUpdatesInitialValues - form initial values depending on requestType + * @returns {{ + * currentContent: string, + * validationSchema: object, + * formTitle: string, + * submitButtonText: string, + * contentFieldName: string + * }} + */ +const geUpdateFormSettings = (requestType, courseUpdatesInitialValues, intl) => { + const updatesValidationSchema = Yup.object().shape({ + id: Yup.number().required(), + date: Yup.date().required(), + content: Yup.string(), + }); + + switch (requestType) { + case REQUEST_TYPES.edit_handouts: + return { + currentContent: courseUpdatesInitialValues.data, + formTitle: intl.formatMessage(messages.editHandoutsTitle), + validationSchema: Yup.object().shape(), + contentFieldName: 'data', + submitButtonText: intl.formatMessage(messages.saveButton), + }; + case REQUEST_TYPES.add_new_update: + return { + currentContent: courseUpdatesInitialValues.content, + formTitle: intl.formatMessage(messages.addNewUpdateTitle), + validationSchema: updatesValidationSchema, + contentFieldName: 'content', + submitButtonText: intl.formatMessage(messages.postButton), + }; + case REQUEST_TYPES.edit_update: + return { + currentContent: courseUpdatesInitialValues.content, + formTitle: intl.formatMessage(messages.editUpdateTitle), + validationSchema: updatesValidationSchema, + contentFieldName: 'content', + submitButtonText: intl.formatMessage(messages.postButton), + }; + default: + return ''; + } +}; + +// eslint-disable-next-line import/prefer-default-export +export { geUpdateFormSettings }; diff --git a/src/course-updates/utils.js b/src/course-updates/utils.js new file mode 100644 index 000000000..f1fc95c46 --- /dev/null +++ b/src/course-updates/utils.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const matchesAnyStatus = (statuses, status) => Object.values(statuses).some(s => s === status); diff --git a/src/generic/WysiwygEditor.scss b/src/generic/WysiwygEditor.scss new file mode 100644 index 000000000..06a122658 --- /dev/null +++ b/src/generic/WysiwygEditor.scss @@ -0,0 +1,5 @@ +.tox-dialog-wrap__backdrop { + background-color: $black !important; + opacity: .5; + z-index: $zindex-modal-backdrop; +} diff --git a/src/generic/processing-notification/ProccessingNotification.scss b/src/generic/processing-notification/ProccessingNotification.scss new file mode 100644 index 000000000..257cbd2af --- /dev/null +++ b/src/generic/processing-notification/ProccessingNotification.scss @@ -0,0 +1,25 @@ +.processing-notification { + display: flex; + position: fixed; + bottom: -13rem; + transition: bottom 1s; + right: 1.25rem; + padding: .625rem 1.25rem; + z-index: $zindex-popover; + + &.is-show { + bottom: .625rem; + } + + .processing-notification-icon { + margin-right: .625rem; + animation: rotate 1s linear infinite; + } + + .processing-notification-title { + font-size: 1rem; + line-height: 1.5rem; + color: $white; + margin-bottom: 0; + } +} diff --git a/src/generic/processing-notification/ProcessingNotification.test.jsx b/src/generic/processing-notification/ProcessingNotification.test.jsx new file mode 100644 index 000000000..16b86401e --- /dev/null +++ b/src/generic/processing-notification/ProcessingNotification.test.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { capitalize } from 'lodash'; +import { NOTIFICATION_MESSAGES } from '../../constants'; +import ProcessingNotification from '.'; + +const props = { + title: NOTIFICATION_MESSAGES.saving, + isShow: true, +}; + +describe('', () => { + it('renders successfully', () => { + const { getByText } = render(); + expect(getByText(capitalize(props.title))).toBeInTheDocument(); + }); +}); diff --git a/src/generic/processing-notification/data/selectors.js b/src/generic/processing-notification/data/selectors.js new file mode 100644 index 000000000..f34ed9df5 --- /dev/null +++ b/src/generic/processing-notification/data/selectors.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line import/prefer-default-export +export const getProcessingNotification = (state) => ({ + isShow: state.processingNotification.isShow, + title: state.processingNotification.title, +}); diff --git a/src/generic/processing-notification/data/slice.js b/src/generic/processing-notification/data/slice.js new file mode 100644 index 000000000..4090524a9 --- /dev/null +++ b/src/generic/processing-notification/data/slice.js @@ -0,0 +1,28 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + isShow: false, + title: '', +}; + +const slice = createSlice({ + name: 'processingNotification', + initialState, + reducers: { + showProcessingNotification: (state, { payload }) => { + state.isShow = true; + state.title = payload; + }, + hideProcessingNotification: () => initialState, + }, +}); + +export const { + showProcessingNotification, + hideProcessingNotification, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/generic/processing-notification/index.jsx b/src/generic/processing-notification/index.jsx new file mode 100644 index 000000000..c58053f45 --- /dev/null +++ b/src/generic/processing-notification/index.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Badge, Icon } from '@edx/paragon'; +import { Settings as IconSettings } from '@edx/paragon/icons'; +import { capitalize } from 'lodash'; + +import { NOTIFICATION_MESSAGES } from '../../constants'; + +const ProcessingNotification = ({ isShow, title }) => ( + + +

+ {capitalize(title)} +

+
+); + +ProcessingNotification.propTypes = { + isShow: PropTypes.bool.isRequired, + title: PropTypes.oneOf(Object.values(NOTIFICATION_MESSAGES)).isRequired, +}; + +export default ProcessingNotification; diff --git a/src/generic/styles.scss b/src/generic/styles.scss index e5dca7bba..4eceeddce 100644 --- a/src/generic/styles.scss +++ b/src/generic/styles.scss @@ -2,3 +2,5 @@ @import "./course-upload-image/CourseUploadImage"; @import "./sub-header/SubHeader"; @import "./section-sub-header/SectionSubHeader"; +@import "./processing-notification/ProccessingNotification"; +@import "./WysiwygEditor"; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index d16a0cea9..2b817efde 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 435a0a818..77f0fb751 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index f5e78c336..bf1703b9b 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index 85458e389..89b2cd2cf 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 84a01ce15..64a333ca3 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index 4f52c1b31..0106fb8fb 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 435a0a818..77f0fb751 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 435a0a818..77f0fb751 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index 4a00f3c66..fd53f7ac0 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 435a0a818..77f0fb751 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index d9456a708..ea886df5c 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index b072f4611..1d3fa2d55 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -642,6 +642,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index 435a0a818..e8ab912b0 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -643,6 +643,28 @@ "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", "course-authoring.schedule.alert.button.saving": "Saving", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-form.new-update-title": "Add new update", + "course-authoring.course-updates.update-form.edit-update-title": "Edit update", + "course-authoring.course-updates.update-form.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-form.date": "Date", + "course-authoring.course-updates.update-form.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-form.error-alt-text": "Error icon", + "course-authoring.course-updates.update-form.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.course-team.headingTitle": "Course team", "course-authoring.course-team.subTitle": "Settings", "course-authoring.course-team.button.new-team-member": "New team member", diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index 435a0a818..77f0fb751 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -678,6 +678,28 @@ "course-authoring.course-team.warning-modal.message": "{email} is already on the {courseName} team. Recheck the email address if you want to add a new member.", "course-authoring.course-team.warning-modal.button.return": "Return to team listing", "course-authoring.advanced-settings.alert.button.saving": "Saving", + "course-authoring.course-updates.header.title": "Course updates", + "course-authoring.course-updates.header.subtitle": "Content", + "course-authoring.course-updates.section-info": "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. You add or edit updates in HTML.", + "course-authoring.course-updates.handouts.title": "Course handouts", + "course-authoring.course-updates.button.new-update": "New update", + "course-authoring.course-updates.button.edit": "Edit", + "course-authoring.course-updates.button.delete": "Delete", + "course-authoring.course-updates.button.save": "Save", + "course-authoring.course-updates.button.cancel": "Cancel", + "course-authoring.course-updates.button.post": "Post", + "course-authoring.course-updates.button.ok": "Ok", + "course-authoring.course-updates.date-invalid": "Action required: Enter a valid date.", + "course-authoring.course-updates.delete-modal.title": "Are you sure you want to delete this update?", + "course-authoring.course-updates.delete-modal.description": "This action cannot be undone.", + "course-authoring.course-updates.update-modal.new-update-title": "Add new update", + "course-authoring.course-updates.update-modal.edit-update-title": "Edit update", + "course-authoring.course-updates.update-modal.edit-handouts-title": "Edit handouts", + "course-authoring.course-updates.update-modal.date": "Date", + "course-authoring.course-updates.update-modal.inValid": "Action required: Enter a valid date.", + "course-authoring.course-updates.update-modal.error-alt-text": "Error icon", + "course-authoring.course-updates.update-modal.calendar-alt-text": "Calendar for datepicker input", + "course-authoring.advanced-settings.alert.button.saving": "Saving", "course-authoring.grading-settings.credit.eligibility.label": "Minimum credit-eligible grade:", "course-authoring.grading-settings.credit.eligibility.description": "% Must be greater than or equal to the course passing grade", "course-authoring.grading-settings.credit.eligibility.error.msg": "Not able to set passing grade to less than:", diff --git a/src/index.scss b/src/index.scss index 91ee8d0ce..6af8bf24d 100755 --- a/src/index.scss +++ b/src/index.scss @@ -7,6 +7,7 @@ @import "assets/scss/variables"; @import "assets/scss/form"; @import "assets/scss/utilities"; +@import "assets/scss/animations"; @import "proctored-exam-settings/proctoredExamSettings"; @import "pages-and-resources/discussions/app-list/AppList"; @import "advanced-settings/scss/AdvancedSettings"; @@ -15,3 +16,4 @@ @import "schedule-and-details/ScheduleAndDetails"; @import "pages-and-resources/PagesAndResources"; @import "course-team/CourseTeam"; +@import "course-updates/CourseUpdates"; diff --git a/src/store.js b/src/store.js index 26b6434e9..515751cbf 100644 --- a/src/store.js +++ b/src/store.js @@ -11,6 +11,8 @@ import { reducer as scheduleAndDetailsReducer } from './schedule-and-details/dat import { reducer as liveReducer } from './pages-and-resources/live/data/slice'; import { reducer as filesReducer } from './files-and-uploads/data/slice'; import { reducer as courseTeamReducer } from './course-team/data/slice'; +import { reducer as CourseUpdatesReducer } from './course-updates/data/slice'; +import { reducer as processingNotificationReducer } from './generic/processing-notification/data/slice'; export default function initializeStore(preloadedState = undefined) { return configureStore({ @@ -26,6 +28,8 @@ export default function initializeStore(preloadedState = undefined) { models: modelsReducer, live: liveReducer, courseTeam: courseTeamReducer, + courseUpdates: CourseUpdatesReducer, + processingNotification: processingNotificationReducer, }, preloadedState, });