feat: Created Course updates page (#581)
This commit is contained in:
1
.env
1
.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/course_info`}>
|
||||
{process.env.ENABLE_NEW_UPDATES_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
)}
|
||||
<CourseUpdates courseId={courseId} />
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/assets`}>
|
||||
<FilesAndUploads courseId={courseId} />
|
||||
|
||||
9
src/assets/scss/_animations.scss
Normal file
9
src/assets/scss/_animations.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -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 = '<p> </p>';
|
||||
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',
|
||||
};
|
||||
|
||||
163
src/course-updates/CourseUpdates.jsx
Normal file
163
src/course-updates/CourseUpdates.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Container size="xl" className="m-4">
|
||||
<section className="setting-items mb-4 mt-5">
|
||||
<Layout
|
||||
lg={[{ span: 12 }]}
|
||||
md={[{ span: 12 }]}
|
||||
sm={[{ span: 12 }]}
|
||||
xs={[{ span: 12 }]}
|
||||
xl={[{ span: 12 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
<article>
|
||||
<div>
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
instruction={intl.formatMessage(messages.sectionInfo)}
|
||||
headerActions={(
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
iconBefore={AddIcon}
|
||||
size="sm"
|
||||
onClick={() => handleOpenUpdateForm(REQUEST_TYPES.add_new_update)}
|
||||
disabled={isUpdateFormOpen}
|
||||
>
|
||||
{intl.formatMessage(messages.newUpdateButton)}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<section className="updates-section">
|
||||
{isMainFormOpen && (
|
||||
<UpdateForm
|
||||
isOpen={isUpdateFormOpen}
|
||||
close={closeUpdateForm}
|
||||
requestType={requestType}
|
||||
onSubmit={handleUpdatesSubmit}
|
||||
courseUpdatesInitialValues={courseUpdatesInitialValues}
|
||||
/>
|
||||
)}
|
||||
<div className="updates-container">
|
||||
<div className="p-4.5">
|
||||
{courseUpdates.length ? courseUpdates.map((courseUpdate, index) => (
|
||||
isInnerFormOpen(courseUpdate.id) ? (
|
||||
<UpdateForm
|
||||
isOpen={isUpdateFormOpen}
|
||||
close={closeUpdateForm}
|
||||
requestType={requestType}
|
||||
isInnerForm
|
||||
isFirstUpdate={index === 0}
|
||||
onSubmit={handleUpdatesSubmit}
|
||||
courseUpdatesInitialValues={courseUpdatesInitialValues}
|
||||
/>
|
||||
) : (
|
||||
<CourseUpdate
|
||||
dateForUpdate={courseUpdate.date}
|
||||
contentForUpdate={courseUpdate.content}
|
||||
onEdit={() => handleOpenUpdateForm(REQUEST_TYPES.edit_update, courseUpdate)}
|
||||
onDelete={() => handleOpenDeleteForm(courseUpdate)}
|
||||
isDisabledButtons={isUpdateFormOpen}
|
||||
/>
|
||||
))) : null}
|
||||
</div>
|
||||
<div className="updates-handouts-container">
|
||||
<CourseHandouts
|
||||
contentForHandouts={courseHandouts?.data || ''}
|
||||
onEdit={() => handleOpenUpdateForm(REQUEST_TYPES.edit_handouts)}
|
||||
isDisabledButtons={isUpdateFormOpen}
|
||||
/>
|
||||
</div>
|
||||
<DeleteModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
close={closeDeleteModal}
|
||||
onDeleteSubmit={handleDeleteUpdateSubmit}
|
||||
/>
|
||||
{isShowProcessingNotification && (
|
||||
<ProcessingNotification
|
||||
isShow={isShowProcessingNotification}
|
||||
title={processingNotificationTitle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
<InternetConnectionAlert
|
||||
isFailed={anyStatusFailed}
|
||||
isQueryPending={anyStatusInProgress || anyStatusPending}
|
||||
onInternetConnectionFailed={() => null}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CourseUpdates.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseUpdates;
|
||||
20
src/course-updates/CourseUpdates.scss
Normal file
20
src/course-updates/CourseUpdates.scss
Normal file
@@ -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;
|
||||
}
|
||||
220
src/course-updates/CourseUpdates.test.jsx
Normal file
220
src/course-updates/CourseUpdates.test.jsx
Normal file
@@ -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: () => <div>Widget</div>,
|
||||
prepareEditorRef: jest.fn(() => ({
|
||||
refReady: true,
|
||||
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
|
||||
})),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseUpdates courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<CourseUpdates />', () => {
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
content: '<p>Some text</p>',
|
||||
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(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
id: courseUpdatesMock[0].id,
|
||||
content: '<p>Some text</p>',
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
const data = {
|
||||
...courseHandoutsMock,
|
||||
data: '<p>Some handouts 1</p>',
|
||||
};
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
83
src/course-updates/__mocks__/courseHandouts.js
Normal file
83
src/course-updates/__mocks__/courseHandouts.js
Normal file
@@ -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: '',
|
||||
},
|
||||
};
|
||||
5
src/course-updates/__mocks__/courseUpdates.js
Normal file
5
src/course-updates/__mocks__/courseUpdates.js
Normal file
@@ -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' },
|
||||
];
|
||||
2
src/course-updates/__mocks__/index.js
Normal file
2
src/course-updates/__mocks__/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as courseUpdatesMock } from './courseUpdates';
|
||||
export { default as courseHandoutsMock } from './courseHandouts';
|
||||
7
src/course-updates/constants.js
Normal file
7
src/course-updates/constants.js
Normal file
@@ -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',
|
||||
};
|
||||
41
src/course-updates/course-handouts/CourseHandouts.jsx
Normal file
41
src/course-updates/course-handouts/CourseHandouts.jsx
Normal file
@@ -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 (
|
||||
<div className="course-handouts" data-testid="course-handouts">
|
||||
<div className="course-handouts-header">
|
||||
<h2 className="course-handouts-header__title lead">{intl.formatMessage(messages.handoutsTitle)}</h2>
|
||||
<Button
|
||||
className="course-handouts-header__btn"
|
||||
data-testid="course-handouts-edit-button"
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
disabled={isDisabledButtons}
|
||||
>
|
||||
{intl.formatMessage(messages.editButton)}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="small"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: contentForHandouts || '' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CourseHandouts.propTypes = {
|
||||
contentForHandouts: PropTypes.string.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
isDisabledButtons: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default CourseHandouts;
|
||||
16
src/course-updates/course-handouts/CourseHandouts.scss
Normal file
16
src/course-updates/course-handouts/CourseHandouts.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/course-updates/course-handouts/CourseHandouts.test.jsx
Normal file
45
src/course-updates/course-handouts/CourseHandouts.test.jsx
Normal file
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<CourseHandouts
|
||||
onEdit={onEditMock}
|
||||
contentForHandouts={handoutsContentMock}
|
||||
isDisabledButtons={false}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('<CourseHandouts />', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
14
src/course-updates/course-handouts/messages.js
Normal file
14
src/course-updates/course-handouts/messages.js
Normal file
@@ -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;
|
||||
64
src/course-updates/course-update/CourseUpdate.jsx
Normal file
64
src/course-updates/course-update/CourseUpdate.jsx
Normal file
@@ -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 (
|
||||
<div className="course-update" data-testid="course-update">
|
||||
<div className="course-update-header">
|
||||
<span className="course-update-header__date small font-weight-bold">{dateForUpdate}</span>
|
||||
{!isDateForUpdateValid(dateForUpdate) && (
|
||||
<div className="course-update-header__error">
|
||||
<Icon src={ErrorIcon} alt={intl.formatMessage(messages.errorMessage)} />
|
||||
<p className="message-error small m-0">{intl.formatMessage(messages.errorMessage)}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="course-update-header__action">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
disabled={isDisabledButtons}
|
||||
data-testid="course-update-edit-button"
|
||||
>
|
||||
{intl.formatMessage(messages.editButton)}
|
||||
</Button>
|
||||
<Button variant="outline-primary" size="sm" onClick={onDelete} disabled={isDisabledButtons}>
|
||||
{intl.formatMessage(messages.deleteButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{Boolean(contentForUpdate) && (
|
||||
<div
|
||||
className="small text-gray-800"
|
||||
data-testid="course-update-content"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: contentForUpdate }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
36
src/course-updates/course-update/CourseUpdate.scss
Normal file
36
src/course-updates/course-update/CourseUpdate.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/course-updates/course-update/CourseUpdate.test.jsx
Normal file
72
src/course-updates/course-update/CourseUpdate.test.jsx
Normal file
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<CourseUpdate
|
||||
dateForUpdate={dateForUpdateMock}
|
||||
contentForUpdate={contentForUpdateMock}
|
||||
onEdit={onEditMock}
|
||||
onDelete={onDeleteMock}
|
||||
isDisabledButtons={false}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('<CourseUpdate />', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
18
src/course-updates/course-update/messages.js
Normal file
18
src/course-updates/course-update/messages.js
Normal file
@@ -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;
|
||||
17
src/course-updates/course-update/utils.js
Normal file
17
src/course-updates/course-update/utils.js
Normal file
@@ -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 };
|
||||
84
src/course-updates/data/api.js
Normal file
84
src/course-updates/data/api.js
Normal file
@@ -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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
export async function editHandouts(courseId, courseHandouts) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.put(getCourseHandoutApiUrl(courseId), courseHandouts);
|
||||
|
||||
return data;
|
||||
}
|
||||
4
src/course-updates/data/selectors.js
Normal file
4
src/course-updates/data/selectors.js
Normal file
@@ -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;
|
||||
72
src/course-updates/data/slice.js
Normal file
72
src/course-updates/data/slice.js
Normal file
@@ -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;
|
||||
111
src/course-updates/data/thunk.js
Normal file
111
src/course-updates/data/thunk.js
Normal file
@@ -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 }));
|
||||
}
|
||||
};
|
||||
}
|
||||
48
src/course-updates/delete-modal/DeleteModal.jsx
Normal file
48
src/course-updates/delete-modal/DeleteModal.jsx
Normal file
@@ -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 (
|
||||
<AlertModal
|
||||
title={intl.formatMessage(messages.deleteModalTitle)}
|
||||
variant="warning"
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={close}>
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDeleteSubmit();
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.okButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p>{intl.formatMessage(messages.deleteModalDescription)}</p>
|
||||
</AlertModal>
|
||||
);
|
||||
};
|
||||
|
||||
DeleteModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
onDeleteSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteModal;
|
||||
47
src/course-updates/delete-modal/DeleteModal.test.jsx
Normal file
47
src/course-updates/delete-modal/DeleteModal.test.jsx
Normal file
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<DeleteModal
|
||||
isOpen
|
||||
close={closeMock}
|
||||
onDeleteSubmit={onDeleteSubmitMock}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('<DeleteModal />', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
22
src/course-updates/delete-modal/messages.js
Normal file
22
src/course-updates/delete-modal/messages.js
Normal file
@@ -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;
|
||||
114
src/course-updates/hooks.jsx
Normal file
114
src/course-updates/hooks.jsx
Normal file
@@ -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 };
|
||||
2
src/course-updates/index.js
Normal file
2
src/course-updates/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as CourseUpdates } from './CourseUpdates';
|
||||
22
src/course-updates/messages.js
Normal file
22
src/course-updates/messages.js
Normal file
@@ -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;
|
||||
141
src/course-updates/update-form/UpdateForm.jsx
Normal file
141
src/course-updates/update-form/UpdateForm.jsx
Normal file
@@ -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 (
|
||||
<div className={classNames('update-form', {
|
||||
'update-form__inner': isInnerForm,
|
||||
'update-form__inner-first': isFirstUpdate,
|
||||
})}
|
||||
>
|
||||
<Formik
|
||||
initialValues={courseUpdatesInitialValues}
|
||||
validationSchema={validationSchema}
|
||||
validateOnMount
|
||||
validateOnBlur
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({
|
||||
values, handleSubmit, isValid, setFieldValue,
|
||||
}) => (
|
||||
<>
|
||||
<h3 className="update-form-title">{formTitle}</h3>
|
||||
{(requestType !== REQUEST_TYPES.edit_handouts) && (
|
||||
<Form.Group className="mb-4 datepicker-field datepicker-custom">
|
||||
<Form.Control.Feedback className="datepicker-float-labels">
|
||||
{intl.formatMessage(messages.updateFormDate)}
|
||||
</Form.Control.Feedback>
|
||||
<div className="position-relative">
|
||||
<Icon
|
||||
src={CalendarIcon}
|
||||
className="datepicker-custom-control-icon"
|
||||
alt={intl.formatMessage(messages.updateFormCalendarAltText)}
|
||||
/>
|
||||
<DatePicker
|
||||
name="date"
|
||||
data-testid="course-updates-datepicker"
|
||||
selected={isValidDate(values.date) ? convertToDateFromString(values.date) : ''}
|
||||
dateFormat={DATE_FORMAT}
|
||||
className={classNames('datepicker-custom-control', {
|
||||
'datepicker-custom-control_isInvalid': !isValid,
|
||||
})}
|
||||
autoComplete="off"
|
||||
selectsStart
|
||||
showPopperArrow={false}
|
||||
onChange={(value) => {
|
||||
if (!isValidDate(value)) {
|
||||
return;
|
||||
}
|
||||
setFieldValue('date', convertToStringFromDate(value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isValid && (
|
||||
<div className="datepicker-field-error">
|
||||
<Icon src={ErrorIcon} alt={intl.formatMessage(messages.updateFormErrorAltText)} />
|
||||
<span className="message-error">{intl.formatMessage(messages.updateFormInValid)}</span>
|
||||
</div>
|
||||
)}
|
||||
</Form.Group>
|
||||
)}
|
||||
<Form.Group className="m-0 mb-3">
|
||||
<WysiwygEditor
|
||||
initialValue={currentContent}
|
||||
data-testid="course-updates-wisiwyg-editor"
|
||||
name={contentFieldName}
|
||||
minHeight={300}
|
||||
onChange={(value) => {
|
||||
setFieldValue(contentFieldName, value || DEFAULT_EMPTY_WYSIWYG_VALUE);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" type="button" onClick={close}>
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} type="submit" disabled={!isValid}>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
63
src/course-updates/update-form/UpdateForm.scss
Normal file
63
src/course-updates/update-form/UpdateForm.scss
Normal file
@@ -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;
|
||||
}
|
||||
140
src/course-updates/update-form/UpdateForm.test.jsx
Normal file
140
src/course-updates/update-form/UpdateForm.test.jsx
Normal file
@@ -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: () => <div>Widget</div>,
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<UpdateForm
|
||||
isOpen
|
||||
close={closeMock}
|
||||
requestType={requestType}
|
||||
onSubmit={onSubmitMock}
|
||||
courseUpdatesInitialValues={courseUpdatesInitialValues(requestType)}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('<UpdateForm />', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
46
src/course-updates/update-form/messages.js
Normal file
46
src/course-updates/update-form/messages.js
Normal file
@@ -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;
|
||||
56
src/course-updates/update-form/utils.js
Normal file
56
src/course-updates/update-form/utils.js
Normal file
@@ -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 };
|
||||
2
src/course-updates/utils.js
Normal file
2
src/course-updates/utils.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const matchesAnyStatus = (statuses, status) => Object.values(statuses).some(s => s === status);
|
||||
5
src/generic/WysiwygEditor.scss
Normal file
5
src/generic/WysiwygEditor.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.tox-dialog-wrap__backdrop {
|
||||
background-color: $black !important;
|
||||
opacity: .5;
|
||||
z-index: $zindex-modal-backdrop;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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('<ProcessingNotification />', () => {
|
||||
it('renders successfully', () => {
|
||||
const { getByText } = render(<ProcessingNotification {...props} />);
|
||||
expect(getByText(capitalize(props.title))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
5
src/generic/processing-notification/data/selectors.js
Normal file
5
src/generic/processing-notification/data/selectors.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getProcessingNotification = (state) => ({
|
||||
isShow: state.processingNotification.isShow,
|
||||
title: state.processingNotification.title,
|
||||
});
|
||||
28
src/generic/processing-notification/data/slice.js
Normal file
28
src/generic/processing-notification/data/slice.js
Normal file
@@ -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;
|
||||
30
src/generic/processing-notification/index.jsx
Normal file
30
src/generic/processing-notification/index.jsx
Normal file
@@ -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 }) => (
|
||||
<Badge
|
||||
className={classNames('processing-notification', {
|
||||
'is-show': isShow,
|
||||
})}
|
||||
variant="secondary"
|
||||
aria-hidden={isShow}
|
||||
>
|
||||
<Icon className="processing-notification-icon" src={IconSettings} />
|
||||
<h2 className="processing-notification-title">
|
||||
{capitalize(title)}
|
||||
</h2>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
ProcessingNotification.propTypes = {
|
||||
isShow: PropTypes.bool.isRequired,
|
||||
title: PropTypes.oneOf(Object.values(NOTIFICATION_MESSAGES)).isRequired,
|
||||
};
|
||||
|
||||
export default ProcessingNotification;
|
||||
@@ -2,3 +2,5 @@
|
||||
@import "./course-upload-image/CourseUploadImage";
|
||||
@import "./sub-header/SubHeader";
|
||||
@import "./section-sub-header/SectionSubHeader";
|
||||
@import "./processing-notification/ProccessingNotification";
|
||||
@import "./WysiwygEditor";
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user