feat: create Studio Home Page MFE (#589)
This commit is contained in:
@@ -32,7 +32,6 @@ ENABLE_ACCESSIBILITY_PAGE=false
|
||||
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||
ENABLE_TEAM_TYPE_SETTING=false
|
||||
ENABLE_NEW_EDITOR_PAGES=true
|
||||
ENABLE_NEW_HOME_PAGE = false
|
||||
ENABLE_NEW_COURSE_OUTLINE_PAGE = false
|
||||
ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
|
||||
ENABLE_NEW_GRADING_PAGE = false
|
||||
|
||||
18
src/AppFooter.jsx
Normal file
18
src/AppFooter.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Footer } from '@edx/frontend-lib-content-components';
|
||||
|
||||
const AppFooter = () => (
|
||||
<div className="mt-6">
|
||||
<Footer
|
||||
marketingBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
||||
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
||||
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
||||
supportEmail={process.env.SUPPORT_EMAIL}
|
||||
platformName={process.env.SITE_NAME}
|
||||
lmsBaseUrl={process.env.LMS_BASE_URL}
|
||||
studioBaseUrl={process.env.STUDIO_BASE_URL}
|
||||
showAccessibilityPage={process.env.ENABLE_ACCESSIBILITY_PAGE === 'true'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AppFooter;
|
||||
17
src/AppFooter.test.jsx
Normal file
17
src/AppFooter.test.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import AppFooter from './AppFooter';
|
||||
|
||||
describe('<AppFooter />', () => {
|
||||
const RootWrapper = () => (
|
||||
<IntlProvider locale="en">
|
||||
<AppFooter />
|
||||
</IntlProvider>
|
||||
);
|
||||
it('should render the footer successfully', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
|
||||
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { Footer } from '@edx/frontend-lib-content-components';
|
||||
import Header from './studio-header/Header';
|
||||
import { fetchCourseDetail } from './data/thunks';
|
||||
import { useModel } from './generic/model-store';
|
||||
@@ -13,6 +12,7 @@ import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
||||
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
|
||||
import { RequestStatus } from './data/constants';
|
||||
import Loading from './generic/Loading';
|
||||
import AppFooter from './AppFooter';
|
||||
|
||||
const AppHeader = ({
|
||||
courseNumber, courseOrg, courseTitle, courseId,
|
||||
@@ -37,21 +37,6 @@ AppHeader.defaultProps = {
|
||||
courseOrg: null,
|
||||
};
|
||||
|
||||
const AppFooter = () => (
|
||||
<div className="mt-6">
|
||||
<Footer
|
||||
marketingBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
||||
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
||||
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
||||
supportEmail={process.env.SUPPORT_EMAIL}
|
||||
platformName={process.env.SITE_NAME}
|
||||
lmsBaseUrl={process.env.LMS_BASE_URL}
|
||||
studioBaseUrl={process.env.STUDIO_BASE_URL}
|
||||
showAccessibilityPage={process.env.ENABLE_ACCESSIBILITY_PAGE === 'true'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
@@ -65,9 +65,7 @@ describe('<CourseAuthoringRoutes>', () => {
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
// TODO: This test needs to be corrected.
|
||||
// The problem arose after moving new commits (https://github.com/raccoongang/frontend-app-course-authoring/pull/25)
|
||||
it.skip('renders the PagesAndResources component when the pages and resources route is active', () => {
|
||||
it('renders the PagesAndResources component when the pages and resources route is active', () => {
|
||||
render(
|
||||
<AppProvider store={store}>
|
||||
<MemoryRouter initialEntries={[`/course/${courseId}/pages-and-resources`]}>
|
||||
@@ -85,9 +83,7 @@ describe('<CourseAuthoringRoutes>', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: This test needs to be corrected.
|
||||
// The problem arose after moving new commits (https://github.com/raccoongang/frontend-app-course-authoring/pull/25)
|
||||
it.skip('renders the ProctoredExamSettings component when the proctored exam settings route is active', () => {
|
||||
it('renders the ProctoredExamSettings component when the proctored exam settings route is active', () => {
|
||||
render(
|
||||
<AppProvider store={store}>
|
||||
<MemoryRouter initialEntries={[`/course/${courseId}/proctored-exam-settings`]}>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import HelpSidebar from '../../generic/help-sidebar';
|
||||
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||
import messages from './messages';
|
||||
|
||||
const SettingsSidebar = ({ intl, courseId, proctoredExamSettingsUrl }) => (
|
||||
|
||||
@@ -79,3 +79,14 @@
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-group-wrapper {
|
||||
position: relative;
|
||||
z-index: $zindex-dropdown;
|
||||
margin-left: auto;
|
||||
|
||||
.dropdown-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ export const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss\\Z';
|
||||
export const COMMA_SEPARATED_DATE_FORMAT = 'MMMM D, YYYY';
|
||||
export const DEFAULT_EMPTY_WYSIWYG_VALUE = '<p> </p>';
|
||||
export const STATEFUL_BUTTON_STATES = {
|
||||
pending: 'pending',
|
||||
default: 'default',
|
||||
pending: 'pending',
|
||||
error: 'error',
|
||||
};
|
||||
|
||||
export const USER_ROLES = {
|
||||
@@ -25,3 +26,11 @@ export const NOTIFICATION_MESSAGES = {
|
||||
};
|
||||
|
||||
export const DEFAULT_TIME_STAMP = '00:00';
|
||||
|
||||
export const COURSE_CREATOR_STATES = {
|
||||
unrequested: 'unrequested',
|
||||
pending: 'pending',
|
||||
granted: 'granted',
|
||||
denied: 'denied',
|
||||
disallowedForThisSite: 'disallowed_for_this_site',
|
||||
};
|
||||
|
||||
93
src/course-rerun/CourseRerun.test.jsx
Normal file
93
src/course-rerun/CourseRerun.test.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { history, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
act, fireEvent, render, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import { studioHomeMock } from '../studio-home/__mocks__';
|
||||
import { getStudioHomeApiUrl } from '../studio-home/data/api';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import messages from './messages';
|
||||
import CourseRerun from '.';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<MemoryRouter>
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseRerun intl={injectIntl} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('<CourseRerun />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
|
||||
it('should render successfully', () => {
|
||||
const { getByText, getAllByRole } = render(<RootWrapper />);
|
||||
expect(getByText(messages.rerunTitle.defaultMessage));
|
||||
expect(getAllByRole('button', { name: messages.cancelButton.defaultMessage }).length).toBe(2);
|
||||
});
|
||||
|
||||
it('should navigate to /home on cancel button click', () => {
|
||||
const { getAllByRole } = render(<RootWrapper />);
|
||||
const cancelButton = getAllByRole('button', { name: messages.cancelButton.defaultMessage })[0];
|
||||
|
||||
fireEvent.click(cancelButton);
|
||||
waitFor(() => {
|
||||
expect(history.location.pathname).toBe('/home');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
useSelector.mockReturnValue({ organizationLoadingStatus: RequestStatus.IN_PROGRESS });
|
||||
|
||||
await act(async () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show footer', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
|
||||
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
58
src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx
Normal file
58
src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { studioHomeMock } from '../../studio-home/__mocks__';
|
||||
import initializeStore from '../../store';
|
||||
import CourseRerunForm from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
let store;
|
||||
|
||||
const onClickCancelMock = jest.fn();
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<CourseRerunForm {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const props = {
|
||||
initialFormValues: {
|
||||
displayName: '',
|
||||
org: '',
|
||||
number: '',
|
||||
run: '',
|
||||
},
|
||||
onClickCancel: onClickCancelMock,
|
||||
};
|
||||
|
||||
describe('<CourseRerunForm />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
|
||||
it('renders description successfully', () => {
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
expect(getByText('Provide identifying information for this re-run of the course. The original course is not affected in any way by a re-run', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
36
src/course-rerun/course-rerun-form/index.jsx
Normal file
36
src/course-rerun/course-rerun-form/index.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { CreateOrRerunCourseForm } from '../../generic/create-or-rerun-course';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseRerunForm = ({ initialFormValues, onClickCancel }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="mb-4.5">
|
||||
<div className="my-2.5">{intl.formatMessage(messages.rerunCourseDescription, {
|
||||
strong: (
|
||||
<strong>{intl.formatMessage(messages.rerunCourseDescriptionStrong)}</strong>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
<CreateOrRerunCourseForm
|
||||
initialValues={initialFormValues}
|
||||
onClickCancel={onClickCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CourseRerunForm.propTypes = {
|
||||
initialFormValues: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
number: PropTypes.string.isRequired,
|
||||
run: PropTypes.string,
|
||||
}).isRequired,
|
||||
onClickCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CourseRerunForm;
|
||||
14
src/course-rerun/course-rerun-form/messages.js
Normal file
14
src/course-rerun/course-rerun-form/messages.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
rerunCourseDescription: {
|
||||
id: 'course-authoring.course-rerun.form.description',
|
||||
defaultMessage: 'Provide identifying information for this re-run of the course. The original course is not affected in any way by a re-run. {strong}',
|
||||
},
|
||||
rerunCourseDescriptionStrong: {
|
||||
id: 'course-authoring.course-rerun.form.description.strong',
|
||||
defaultMessage: 'Note: Together, the organization, course number, and course run must uniquely identify this new course instance.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import CourseRerunSideBar from '.';
|
||||
import messages from './messages';
|
||||
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store} messages={{}}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseRerunSideBar courseId={courseId} {...props} />
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<CourseRerunSideBar />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('render CourseRerunSideBar successfully', () => {
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
expect(getByText(messages.sectionTitle1.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.sectionDescription1.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.sectionTitle2.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.sectionDescription2.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.sectionTitle3.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.sectionDescription3.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.sectionLink4.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
66
src/course-rerun/course-rerun-sidebar/index.jsx
Normal file
66
src/course-rerun/course-rerun-sidebar/index.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useHelpUrls } from '../../help-urls/hooks';
|
||||
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseRerunSideBar = () => {
|
||||
const intl = useIntl();
|
||||
const { default: learnMoreUrl } = useHelpUrls(['default']);
|
||||
|
||||
const sidebarMessages = [
|
||||
{
|
||||
title: intl.formatMessage(messages.sectionTitle1),
|
||||
description: intl.formatMessage(messages.sectionDescription1),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage(messages.sectionTitle2),
|
||||
description: intl.formatMessage(messages.sectionDescription2),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage(messages.sectionTitle3),
|
||||
description: intl.formatMessage(messages.sectionDescription3),
|
||||
},
|
||||
{
|
||||
link: {
|
||||
text: intl.formatMessage(messages.sectionLink4),
|
||||
href: learnMoreUrl,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<HelpSidebar
|
||||
intl={intl}
|
||||
showOtherSettings={false}
|
||||
className="mt-3"
|
||||
>
|
||||
{sidebarMessages.map(({ title, description, link }, index) => {
|
||||
const isLastSection = index === sidebarMessages.length - 1;
|
||||
|
||||
return (
|
||||
<div key={uuid()}>
|
||||
<h4 className="help-sidebar-about-title">{title}</h4>
|
||||
<p className="help-sidebar-about-descriptions">{description}</p>
|
||||
{!!link && (
|
||||
<Hyperlink
|
||||
className="small"
|
||||
destination={link.href || ''}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{link.text}
|
||||
</Hyperlink>
|
||||
)}
|
||||
{!isLastSection && <hr className="my-3.5" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</HelpSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseRerunSideBar;
|
||||
34
src/course-rerun/course-rerun-sidebar/messages.js
Normal file
34
src/course-rerun/course-rerun-sidebar/messages.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
sectionTitle1: {
|
||||
id: 'course-authoring.course-rerun.sidebar.section-1.title',
|
||||
defaultMessage: 'When will my course re-run start?',
|
||||
},
|
||||
sectionDescription1: {
|
||||
id: 'course-authoring.course-rerun.sidebar.section-1.description',
|
||||
defaultMessage: 'The new course is set to start on January 1, 2030 at midnight (UTC).',
|
||||
},
|
||||
sectionTitle2: {
|
||||
id: 'course-authoring.course-rerun.sidebar.section-2.title',
|
||||
defaultMessage: 'What transfers from the original course?',
|
||||
},
|
||||
sectionDescription2: {
|
||||
id: 'course-authoring.course-rerun.sidebar.section-2.description',
|
||||
defaultMessage: 'The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.',
|
||||
},
|
||||
sectionTitle3: {
|
||||
id: 'course-authoring.course-rerun.sidebar.section-3.title',
|
||||
defaultMessage: 'What does not transfer from the original course?',
|
||||
},
|
||||
sectionDescription3: {
|
||||
id: 'course-authoring.course-rerun.sidebar.section-3.description',
|
||||
defaultMessage: 'You are the only member of the new course\'s staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.',
|
||||
},
|
||||
sectionLink4: {
|
||||
id: 'course-authoring.course-rerun.sidebar.section-4.link',
|
||||
defaultMessage: 'Learn more about course re-runs',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
67
src/course-rerun/hooks.jsx
Normal file
67
src/course-rerun/hooks.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { updateSavingStatus } from '../generic/data/slice';
|
||||
import {
|
||||
getSavingStatus,
|
||||
getRedirectUrlObj,
|
||||
getCourseRerunData,
|
||||
getCourseData,
|
||||
} from '../generic/data/selectors';
|
||||
import { fetchCourseRerunQuery, fetchOrganizationsQuery } from '../generic/data/thunks';
|
||||
import { fetchStudioHomeData } from '../studio-home/data/thunks';
|
||||
|
||||
const useCourseRerun = (courseId) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const courseData = useSelector(getCourseData);
|
||||
const courseRerunData = useSelector(getCourseRerunData);
|
||||
const redirectUrlObj = useSelector(getRedirectUrlObj);
|
||||
|
||||
const {
|
||||
displayName = '',
|
||||
org = '',
|
||||
run = '',
|
||||
number = '',
|
||||
} = courseRerunData;
|
||||
const originalCourseData = `${org} ${number} ${run}`;
|
||||
const initialFormValues = {
|
||||
displayName,
|
||||
org,
|
||||
number,
|
||||
run: '',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStudioHomeData());
|
||||
dispatch(fetchCourseRerunQuery(courseId));
|
||||
dispatch(fetchOrganizationsQuery());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateSavingStatus({ status: '' }));
|
||||
const { url } = redirectUrlObj;
|
||||
if (url) {
|
||||
history.push('/home');
|
||||
}
|
||||
}
|
||||
}, [savingStatus]);
|
||||
|
||||
return {
|
||||
intl,
|
||||
courseData,
|
||||
displayName,
|
||||
savingStatus,
|
||||
initialFormValues,
|
||||
originalCourseData,
|
||||
dispatch,
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useCourseRerun };
|
||||
99
src/course-rerun/index.jsx
Normal file
99
src/course-rerun/index.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Container,
|
||||
Layout,
|
||||
Stack,
|
||||
ActionRow,
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
import Header from '../studio-header/Header';
|
||||
import Loading from '../generic/Loading';
|
||||
import { getLoadingStatuses } from '../generic/data/selectors';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import AppFooter from '../AppFooter';
|
||||
import CourseRerunForm from './course-rerun-form';
|
||||
import CourseRerunSideBar from './course-rerun-sidebar';
|
||||
import messages from './messages';
|
||||
import { useCourseRerun } from './hooks';
|
||||
|
||||
const CourseRerun = ({ courseId }) => {
|
||||
const {
|
||||
intl,
|
||||
displayName,
|
||||
savingStatus,
|
||||
initialFormValues,
|
||||
originalCourseData,
|
||||
} = useCourseRerun(courseId);
|
||||
const { organizationLoadingStatus } = useSelector(getLoadingStatuses);
|
||||
|
||||
if (organizationLoadingStatus === RequestStatus.IN_PROGRESS) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const handleRerunCourseCancel = () => {
|
||||
history.push('/home');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header isHiddenMainMenu />
|
||||
<Container size="xl" className="m-4">
|
||||
<section className="mb-4">
|
||||
<article>
|
||||
<section>
|
||||
<header className="d-flex">
|
||||
<h3 className="align-self-center font-weight-normal mb-0">{intl.formatMessage(messages.rerunTitle)}</h3>
|
||||
<ActionRow className="ml-auto">
|
||||
<Button variant="outline-primary" onClick={handleRerunCourseCancel}>
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</header>
|
||||
<hr />
|
||||
<Stack>
|
||||
<h3>{originalCourseData}</h3>
|
||||
<h2>{displayName}</h2>
|
||||
</Stack>
|
||||
<hr />
|
||||
</section>
|
||||
</article>
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
sm={[{ span: 9 }, { span: 3 }]}
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
<CourseRerunForm
|
||||
initialFormValues={initialFormValues}
|
||||
onClickCancel={handleRerunCourseCancel}
|
||||
/>
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<CourseRerunSideBar />
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
<InternetConnectionAlert
|
||||
isFailed={savingStatus === RequestStatus.FAILED}
|
||||
isQueryPending={savingStatus === RequestStatus.PENDING}
|
||||
/>
|
||||
</div>
|
||||
<AppFooter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CourseRerun.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseRerun;
|
||||
14
src/course-rerun/messages.js
Normal file
14
src/course-rerun/messages.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
rerunTitle: {
|
||||
id: 'course-authoring.course-rerun.title',
|
||||
defaultMessage: 'Create a re-run of a course',
|
||||
},
|
||||
cancelButton: {
|
||||
id: 'course-authoring.course-rerun.actions.button.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import HelpSidebar from '../../generic/help-sidebar';
|
||||
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseTeamSideBar = ({ courseId, isOwnershipHint, isShowInitialSidebar }) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import HelpSidebar from '../../generic/help-sidebar';
|
||||
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||
import { useHelpUrls } from '../../help-urls/hooks';
|
||||
import messages from './messages';
|
||||
|
||||
|
||||
296
src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx
Normal file
296
src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx
Normal file
@@ -0,0 +1,296 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useParams } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Form,
|
||||
Button,
|
||||
Dropdown,
|
||||
ActionRow,
|
||||
StatefulButton,
|
||||
TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
import { Info as InfoIcon } from '@edx/paragon/icons';
|
||||
import { TypeaheadDropdown } from '@edx/frontend-lib-content-components';
|
||||
|
||||
import AlertMessage from '../alert-message';
|
||||
import { STATEFUL_BUTTON_STATES } from '../../constants';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { getSavingStatus } from '../data/selectors';
|
||||
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
||||
import { updatePostErrors } from '../data/slice';
|
||||
import { updateCreateOrRerunCourseQuery } from '../data/thunks';
|
||||
import { useCreateOrRerunCourse } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
const CreateOrRerunCourseForm = ({
|
||||
title,
|
||||
isCreateNewCourse,
|
||||
initialValues,
|
||||
onClickCancel,
|
||||
}) => {
|
||||
const { courseId } = useParams();
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const { allowToCreateNewOrg } = useSelector(getStudioHomeData);
|
||||
const runFieldReference = useRef(null);
|
||||
const displayNameFieldReference = useRef(null);
|
||||
|
||||
const {
|
||||
intl,
|
||||
errors,
|
||||
values,
|
||||
postErrors,
|
||||
isFormFilled,
|
||||
isFormInvalid,
|
||||
organizations,
|
||||
showErrorBanner,
|
||||
dispatch,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
hasErrorField,
|
||||
setFieldValue,
|
||||
} = useCreateOrRerunCourse(initialValues);
|
||||
|
||||
const newCourseFields = [
|
||||
{
|
||||
label: intl.formatMessage(messages.courseDisplayNameLabel),
|
||||
helpText: intl.formatMessage(
|
||||
isCreateNewCourse
|
||||
? messages.courseDisplayNameCreateHelpText
|
||||
: messages.courseDisplayNameRerunHelpText,
|
||||
),
|
||||
name: 'displayName',
|
||||
value: values.displayName,
|
||||
placeholder: intl.formatMessage(messages.courseDisplayNamePlaceholder),
|
||||
disabled: false,
|
||||
ref: displayNameFieldReference,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(messages.courseOrgLabel),
|
||||
helpText: isCreateNewCourse
|
||||
? intl.formatMessage(messages.courseOrgCreateHelpText, {
|
||||
strong: <strong>{intl.formatMessage(messages.courseNoteOrgNameIsPartStrong)}</strong>,
|
||||
})
|
||||
: intl.formatMessage(messages.courseOrgRerunHelpText, {
|
||||
strong: (
|
||||
<>
|
||||
<br />
|
||||
<strong>
|
||||
{intl.formatMessage(messages.courseNoteNoSpaceAllowedStrong)}
|
||||
</strong>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
name: 'org',
|
||||
value: values.org,
|
||||
options: organizations,
|
||||
placeholder: intl.formatMessage(messages.courseOrgPlaceholder),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(messages.courseNumberLabel),
|
||||
helpText: isCreateNewCourse
|
||||
? intl.formatMessage(messages.courseNumberCreateHelpText, {
|
||||
strong: (
|
||||
<strong>
|
||||
{intl.formatMessage(messages.courseNotePartCourseURLRequireStrong)}
|
||||
</strong>
|
||||
),
|
||||
})
|
||||
: intl.formatMessage(messages.courseNumberRerunHelpText),
|
||||
name: 'number',
|
||||
value: values.number,
|
||||
placeholder: intl.formatMessage(messages.courseNumberPlaceholder),
|
||||
disabled: !isCreateNewCourse,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(messages.courseRunLabel),
|
||||
helpText: isCreateNewCourse
|
||||
? intl.formatMessage(messages.courseRunCreateHelpText, {
|
||||
strong: (
|
||||
<strong>
|
||||
{intl.formatMessage(messages.courseNotePartCourseURLRequireStrong)}
|
||||
</strong>
|
||||
),
|
||||
})
|
||||
: intl.formatMessage(messages.courseRunRerunHelpText, {
|
||||
strong: (
|
||||
<>
|
||||
<br />
|
||||
<strong>
|
||||
{intl.formatMessage(messages.courseNoteNoSpaceAllowedStrong)}
|
||||
</strong>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
name: 'run',
|
||||
value: values.run,
|
||||
placeholder: intl.formatMessage(messages.courseRunPlaceholder),
|
||||
disabled: false,
|
||||
ref: runFieldReference,
|
||||
},
|
||||
];
|
||||
|
||||
const createButtonState = {
|
||||
labels: {
|
||||
default: intl.formatMessage(isCreateNewCourse ? messages.createButton : messages.rerunCreateButton),
|
||||
pending: intl.formatMessage(isCreateNewCourse ? messages.creatingButton : messages.rerunningCreateButton),
|
||||
},
|
||||
disabledStates: [STATEFUL_BUTTON_STATES.pending],
|
||||
};
|
||||
|
||||
const handleOnClickCreate = () => {
|
||||
const courseData = isCreateNewCourse ? values : { ...values, sourceCourseKey: courseId };
|
||||
dispatch(updateCreateOrRerunCourseQuery(courseData));
|
||||
};
|
||||
|
||||
const handleOnClickCancel = () => {
|
||||
dispatch(updatePostErrors({}));
|
||||
onClickCancel();
|
||||
};
|
||||
|
||||
const handleCustomBlurForDropdown = (e) => {
|
||||
// it needs to correct handleOnChange Form.Autosuggest
|
||||
const { value, name } = e.target;
|
||||
setFieldValue(name, value);
|
||||
handleBlur(e);
|
||||
};
|
||||
|
||||
const renderOrgField = (field) => (allowToCreateNewOrg ? (
|
||||
<TypeaheadDropdown
|
||||
readOnly={false}
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
controlClassName={classNames({ 'is-invalid': hasErrorField(field.name) })}
|
||||
options={field.options}
|
||||
placeholder={field.placeholder}
|
||||
handleBlur={handleCustomBlurForDropdown}
|
||||
handleChange={(value) => setFieldValue(field.name, value)}
|
||||
noOptionsMessage={intl.formatMessage(messages.courseOrgNoOptions)}
|
||||
helpMessage=""
|
||||
errorMessage=""
|
||||
floatingLabel=""
|
||||
/>
|
||||
) : (
|
||||
<Dropdown className="mr-2">
|
||||
<Dropdown.Toggle id={`${field.name}-dropdown`} variant="outline-primary">
|
||||
{field.value || intl.formatMessage(messages.courseOrgNoOptions)}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{field.options?.map((value) => (
|
||||
<Dropdown.Item
|
||||
key={value}
|
||||
onClick={() => setFieldValue(field.name, value)}
|
||||
>
|
||||
{value}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
));
|
||||
|
||||
useEffect(() => {
|
||||
// it needs to display the initial focus for the field depending on the current page
|
||||
if (!isCreateNewCourse) {
|
||||
runFieldReference?.current?.focus();
|
||||
} else {
|
||||
displayNameFieldReference?.current?.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="create-or-rerun-course-form">
|
||||
<TransitionReplace>
|
||||
{showErrorBanner ? (
|
||||
<AlertMessage
|
||||
variant="danger"
|
||||
icon={InfoIcon}
|
||||
title={postErrors.errMsg}
|
||||
aria-hidden="true"
|
||||
aria-labelledby={intl.formatMessage(
|
||||
messages.alertErrorExistsAriaLabelledBy,
|
||||
)}
|
||||
aria-describedby={intl.formatMessage(
|
||||
messages.alertErrorExistsAriaDescribedBy,
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</TransitionReplace>
|
||||
<h3 className="mb-3">{title}</h3>
|
||||
<Form>
|
||||
{newCourseFields.map((field) => (
|
||||
<Form.Group
|
||||
className={classNames('form-group-custom', {
|
||||
'form-group-custom_isInvalid': hasErrorField(field.name),
|
||||
})}
|
||||
key={field.label}
|
||||
>
|
||||
<Form.Label>{field.label}</Form.Label>
|
||||
{field.name !== 'org' ? (
|
||||
<Form.Control
|
||||
value={field.value}
|
||||
placeholder={field.placeholder}
|
||||
name={field.name}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
isInvalid={hasErrorField(field.name)}
|
||||
disabled={field.disabled}
|
||||
ref={field?.ref}
|
||||
/>
|
||||
) : renderOrgField(field)}
|
||||
<Form.Text>{field.helpText}</Form.Text>
|
||||
{hasErrorField(field.name) && (
|
||||
<Form.Control.Feedback
|
||||
className="feedback-error"
|
||||
type="invalid"
|
||||
hasIcon={false}
|
||||
>
|
||||
<span className="x-small">{errors[field.name]}</span>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
))}
|
||||
<ActionRow className="justify-content-start">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleOnClickCancel}
|
||||
>
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</Button>
|
||||
<StatefulButton
|
||||
key="save-button"
|
||||
className="ml-3"
|
||||
onClick={handleOnClickCreate}
|
||||
disabled={!isFormFilled || isFormInvalid}
|
||||
state={
|
||||
savingStatus === RequestStatus.PENDING
|
||||
? STATEFUL_BUTTON_STATES.pending
|
||||
: STATEFUL_BUTTON_STATES.default
|
||||
}
|
||||
{...createButtonState}
|
||||
/>
|
||||
</ActionRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CreateOrRerunCourseForm.defaultProps = {
|
||||
title: '',
|
||||
isCreateNewCourse: false,
|
||||
};
|
||||
|
||||
CreateOrRerunCourseForm.propTypes = {
|
||||
title: PropTypes.string,
|
||||
initialValues: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
number: PropTypes.string.isRequired,
|
||||
run: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
isCreateNewCourse: PropTypes.bool,
|
||||
onClickCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CreateOrRerunCourseForm;
|
||||
@@ -0,0 +1,18 @@
|
||||
.create-or-rerun-course-form {
|
||||
.form-group-custom {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
|
||||
.pgn__form-label {
|
||||
font: normal 1.125rem/1.75rem $font-family-base;
|
||||
color: $gray-700;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.pgn__form-control-description,
|
||||
.pgn__form-text {
|
||||
margin-top: .62rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { studioHomeMock } from '../../studio-home/__mocks__';
|
||||
import { getStudioHomeApiUrl } from '../../studio-home/data/api';
|
||||
import { fetchStudioHomeData } from '../../studio-home/data/thunks';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { updateCreateOrRerunCourseQuery } from '../data/thunks';
|
||||
import { getCreateOrRerunCourseUrl } from '../data/api';
|
||||
import messages from './messages';
|
||||
import { CreateOrRerunCourseForm } from '.';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useParams: () => ({
|
||||
courseId: 'course-id-mock',
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
const onClickCancelMock = jest.fn();
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<CreateOrRerunCourseForm {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const props = {
|
||||
title: 'Mocked title',
|
||||
isCreateNewCourse: true,
|
||||
initialValues: {
|
||||
displayName: '',
|
||||
org: '',
|
||||
number: '',
|
||||
run: '',
|
||||
},
|
||||
onClickCancel: onClickCancelMock,
|
||||
};
|
||||
|
||||
describe('<CreateOrRerunCourseForm />', async () => {
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||
axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200);
|
||||
|
||||
await executeThunk(fetchStudioHomeData, store.dispatch);
|
||||
await executeThunk(updateCreateOrRerunCourseQuery, store.dispatch);
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
|
||||
it('renders form successfully', () => {
|
||||
const { getByText, getByPlaceholderText } = render(
|
||||
<RootWrapper {...props} />,
|
||||
);
|
||||
expect(getByText(props.title)).toBeInTheDocument();
|
||||
expect(getByText(messages.courseDisplayNameLabel.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(getByText(messages.courseOrgLabel.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.courseOrgNoOptions.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(getByText(messages.courseNumberLabel.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(getByText(messages.courseRunLabel.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders create course form with help text successfully', () => {
|
||||
const { getByText, getByRole } = render(<RootWrapper {...props} />);
|
||||
expect(getByText(messages.courseDisplayNameCreateHelpText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText('The name of the organization sponsoring the course.', { exact: false })).toBeInTheDocument();
|
||||
expect(getByText('The unique number that identifies your course within your organization.', { exact: false })).toBeInTheDocument();
|
||||
expect(getByText('The term in which your course will run.', { exact: false })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.createButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders rerun course form with help text successfully', () => {
|
||||
const initialProps = { ...props, isCreateNewCourse: false };
|
||||
const { getByText, getByRole } = render(
|
||||
<RootWrapper {...initialProps} />,
|
||||
);
|
||||
expect(getByText(messages.courseDisplayNameRerunHelpText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText('The name of the organization sponsoring the new course. (This name is often the same as the original organization name.)', { exact: false })).toBeInTheDocument();
|
||||
expect(getByText(messages.courseNumberRerunHelpText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText('The term in which the new course will run. (This value is often different than the original course run value.)', { exact: false })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.rerunCreateButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call handleOnClickCancel if button cancel clicked', async () => {
|
||||
const { getByRole } = render(<RootWrapper {...props} />);
|
||||
const cancelBtn = getByRole('button', { name: messages.cancelButton.defaultMessage });
|
||||
act(() => {
|
||||
fireEvent.click(cancelBtn);
|
||||
});
|
||||
expect(onClickCancelMock).toHaveBeenCalled();
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
{
|
||||
payload: {},
|
||||
type: 'generic/updatePostErrors',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should call handleOnClickCreate if button create clicked', async () => {
|
||||
const { getByPlaceholderText, getByText, getByRole } = render(<RootWrapper {...props} />);
|
||||
const displayNameInput = getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage);
|
||||
const orgInput = getByText(messages.courseOrgNoOptions.defaultMessage);
|
||||
const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||
const runInput = getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage);
|
||||
const createBtn = getByRole('button', { name: messages.createButton.defaultMessage });
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(displayNameInput, { target: { value: 'foo course name' } });
|
||||
fireEvent.click(orgInput);
|
||||
fireEvent.change(numberInput, { target: { value: '777' } });
|
||||
fireEvent.change(runInput, { target: { value: '1' } });
|
||||
fireEvent.click(createBtn);
|
||||
});
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
{
|
||||
payload: {},
|
||||
type: 'generic/updatePostErrors',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should be disabled create button if form not filled', () => {
|
||||
const { getByRole } = render(<RootWrapper {...props} />);
|
||||
const createBtn = getByRole('button', { name: messages.createButton.defaultMessage });
|
||||
expect(createBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should be disabled rerun button if form not filled', () => {
|
||||
const initialProps = { ...props, isCreateNewCourse: false };
|
||||
const { getByRole } = render(<RootWrapper {...initialProps} />);
|
||||
const rerunBtn = getByRole('button', { name: messages.rerunCreateButton.defaultMessage });
|
||||
expect(rerunBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should be disabled create button if form has error', () => {
|
||||
const { getByRole, getByPlaceholderText, getByText } = render(<RootWrapper {...props} />);
|
||||
const createBtn = getByRole('button', { name: messages.createButton.defaultMessage });
|
||||
const displayNameInput = getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage);
|
||||
const orgInput = getByText(messages.courseOrgNoOptions.defaultMessage);
|
||||
const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||
const runInput = getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(displayNameInput, { target: { value: 'foo course name' } });
|
||||
fireEvent.click(orgInput);
|
||||
fireEvent.change(numberInput, { target: { value: 'number with invalid (+) symbol' } });
|
||||
fireEvent.change(runInput, { target: { value: 'number with invalid (=) symbol' } });
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(createBtn).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows typeahead dropdown with allowed to create org permissions', () => {
|
||||
useSelector.mockReturnValue({ ...studioHomeMock, allowToCreateNewOrg: true });
|
||||
const { getByPlaceholderText } = render(<RootWrapper {...props} />);
|
||||
expect(getByPlaceholderText(messages.courseOrgPlaceholder.defaultMessage));
|
||||
});
|
||||
|
||||
it('shows button pending state', () => {
|
||||
useSelector.mockReturnValue(RequestStatus.PENDING);
|
||||
const { getByRole } = render(<RootWrapper {...props} />);
|
||||
expect(getByRole('button', { name: messages.creatingButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
it('shows alert error if postErrors presents', () => {
|
||||
useSelector.mockReturnValue({
|
||||
errMsg: 'aaa',
|
||||
orgErrMsg: 'bbb',
|
||||
courseErrMsg: 'ccc',
|
||||
});
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
expect(getByText('aaa')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error on field', () => {
|
||||
const { getByPlaceholderText, getByText } = render(<RootWrapper {...props} />);
|
||||
const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(numberInput, { target: { value: 'number with invalid (+) symbol' } });
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(getByText(messages.noSpaceError)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
4
src/generic/create-or-rerun-course/constants.js
Normal file
4
src/generic/create-or-rerun-course/constants.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const redirectToCourseIndex = (url) => `${url}/outline`;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { redirectToCourseIndex };
|
||||
122
src/generic/create-or-rerun-course/hooks.jsx
Normal file
122
src/generic/create-or-rerun-course/hooks.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
||||
import {
|
||||
getRedirectUrlObj,
|
||||
getOrganizations,
|
||||
getPostErrors,
|
||||
getSavingStatus,
|
||||
} from '../data/selectors';
|
||||
import { updateSavingStatus, updatePostErrors } from '../data/slice';
|
||||
import { fetchOrganizationsQuery } from '../data/thunks';
|
||||
import { redirectToCourseIndex } from './constants';
|
||||
import messages from './messages';
|
||||
|
||||
const useCreateOrRerunCourse = (initialValues) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const redirectUrlObj = useSelector(getRedirectUrlObj);
|
||||
const createOrRerunCourseSavingStatus = useSelector(getSavingStatus);
|
||||
const allOrganizations = useSelector(getOrganizations);
|
||||
const postErrors = useSelector(getPostErrors);
|
||||
const {
|
||||
allowToCreateNewOrg,
|
||||
allowedOrganizations,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const [isFormFilled, setFormFilled] = useState(false);
|
||||
const [showErrorBanner, setShowErrorBanner] = useState(false);
|
||||
const organizations = allowToCreateNewOrg ? allOrganizations : allowedOrganizations;
|
||||
const specialCharsRule = /^[a-zA-Z0-9_\-.'*~\s]+$/;
|
||||
const noSpaceRule = /^\S*$/;
|
||||
const validationSchema = Yup.object().shape({
|
||||
displayName: Yup.string().required(
|
||||
intl.formatMessage(messages.requiredFieldError),
|
||||
),
|
||||
org: Yup.string()
|
||||
.required(intl.formatMessage(messages.requiredFieldError))
|
||||
.matches(
|
||||
specialCharsRule,
|
||||
intl.formatMessage(messages.disallowedCharsError),
|
||||
)
|
||||
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
||||
number: Yup.string()
|
||||
.required(intl.formatMessage(messages.requiredFieldError))
|
||||
.matches(
|
||||
specialCharsRule,
|
||||
intl.formatMessage(messages.disallowedCharsError),
|
||||
)
|
||||
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
||||
run: Yup.string()
|
||||
.required(intl.formatMessage(messages.requiredFieldError))
|
||||
.matches(
|
||||
specialCharsRule,
|
||||
intl.formatMessage(messages.disallowedCharsError),
|
||||
)
|
||||
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
||||
});
|
||||
|
||||
const {
|
||||
values, errors, touched, handleChange, handleBlur, setFieldValue,
|
||||
} = useFormik({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
validateOnBlur: false,
|
||||
validationSchema,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (allowToCreateNewOrg) {
|
||||
dispatch(fetchOrganizationsQuery());
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setFormFilled(Object.values(values).every((i) => i));
|
||||
dispatch(updatePostErrors({}));
|
||||
}, [values]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowErrorBanner(!!postErrors.errMsg);
|
||||
}, [postErrors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (createOrRerunCourseSavingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateSavingStatus({ status: '' }));
|
||||
const { url } = redirectUrlObj;
|
||||
if (url) {
|
||||
history.push(redirectToCourseIndex(url));
|
||||
}
|
||||
} else if (createOrRerunCourseSavingStatus === RequestStatus.FAILED) {
|
||||
dispatch(updateSavingStatus({ status: '' }));
|
||||
}
|
||||
}, [createOrRerunCourseSavingStatus]);
|
||||
|
||||
const hasErrorField = (fieldName) => !!errors[fieldName] && !!touched[fieldName];
|
||||
const isFormInvalid = Object.keys(errors).length;
|
||||
|
||||
return {
|
||||
intl,
|
||||
errors,
|
||||
values,
|
||||
postErrors,
|
||||
isFormFilled,
|
||||
isFormInvalid,
|
||||
organizations,
|
||||
showErrorBanner,
|
||||
dispatch,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
hasErrorField,
|
||||
setFieldValue,
|
||||
setShowErrorBanner,
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useCreateOrRerunCourse };
|
||||
2
src/generic/create-or-rerun-course/index.js
Normal file
2
src/generic/create-or-rerun-course/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as CreateOrRerunCourseForm } from './CreateOrRerunCourseForm';
|
||||
export { useCreateOrRerunCourse } from './hooks';
|
||||
130
src/generic/create-or-rerun-course/messages.js
Normal file
130
src/generic/create-or-rerun-course/messages.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
courseDisplayNameLabel: {
|
||||
id: 'course-authoring.create-or-rerun-course.display-name.label',
|
||||
defaultMessage: 'Course name',
|
||||
},
|
||||
courseDisplayNamePlaceholder: {
|
||||
id: 'course-authoring.create-or-rerun-course.display-name.placeholder',
|
||||
defaultMessage: 'e.g. Introduction to Computer Science',
|
||||
},
|
||||
courseDisplayNameCreateHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.create.display-name.help-text',
|
||||
defaultMessage: 'The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.',
|
||||
},
|
||||
courseDisplayNameRerunHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.rerun.display-name.help-text',
|
||||
defaultMessage: 'The public display name for the new course. (This name is often the same as the original course name.)',
|
||||
},
|
||||
courseOrgLabel: {
|
||||
id: 'course-authoring.create-or-rerun-course.org.label',
|
||||
defaultMessage: 'Organization',
|
||||
},
|
||||
courseOrgPlaceholder: {
|
||||
id: 'course-authoring.create-or-rerun-course.org.placeholder',
|
||||
defaultMessage: 'e.g. UniversityX or OrganizationX',
|
||||
},
|
||||
courseOrgNoOptions: {
|
||||
id: 'course-authoring.create-or-rerun-course.org.no-options',
|
||||
defaultMessage: 'No options',
|
||||
},
|
||||
courseOrgCreateHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.create.org.help-text',
|
||||
defaultMessage: 'The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.',
|
||||
},
|
||||
courseOrgRerunHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.rerun.org.help-text',
|
||||
defaultMessage: 'The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}',
|
||||
},
|
||||
courseNoteNoSpaceAllowedStrong: {
|
||||
id: 'course-authoring.create-or-rerun-course.no-space-allowed.strong',
|
||||
defaultMessage: 'Note: No spaces or special characters are allowed.',
|
||||
},
|
||||
courseNoteOrgNameIsPartStrong: {
|
||||
id: 'course-authoring.create-or-rerun-course.org.help-text.strong',
|
||||
defaultMessage: 'Note: The organization name is part of the course URL.',
|
||||
},
|
||||
courseNumberLabel: {
|
||||
id: 'course-authoring.create-or-rerun-course.number.label',
|
||||
defaultMessage: 'Course number',
|
||||
},
|
||||
courseNumberPlaceholder: {
|
||||
id: 'course-authoring.create-or-rerun-course.number.placeholder',
|
||||
defaultMessage: 'e.g. CS101',
|
||||
},
|
||||
courseNumberCreateHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.create.number.help-text',
|
||||
defaultMessage: 'The unique number that identifies your course within your organization. {strong}',
|
||||
},
|
||||
courseNumberRerunHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.rerun.number.help-text',
|
||||
defaultMessage: 'The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)',
|
||||
},
|
||||
courseNotePartCourseURLRequireStrong: {
|
||||
id: 'course-authoring.create-or-rerun-course.number.help-text.strong',
|
||||
defaultMessage: 'Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.',
|
||||
},
|
||||
courseRunLabel: {
|
||||
id: 'course-authoring.create-or-rerun-course.run.label',
|
||||
defaultMessage: 'Course run',
|
||||
},
|
||||
courseRunPlaceholder: {
|
||||
id: 'course-authoring.create-or-rerun-course.run.placeholder',
|
||||
defaultMessage: 'e.g. 2014_T1',
|
||||
},
|
||||
courseRunCreateHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.create.run.help-text',
|
||||
defaultMessage: 'The term in which your course will run. {strong}',
|
||||
},
|
||||
courseRunRerunHelpText: {
|
||||
id: 'course-authoring.create-or-rerun-course.create.rerun.help-text',
|
||||
defaultMessage: 'The term in which the new course will run. (This value is often different than the original course run value.){strong}',
|
||||
},
|
||||
defaultPlaceholder: {
|
||||
id: 'course-authoring.create-or-rerun-course.default-placeholder',
|
||||
defaultMessage: 'Label',
|
||||
},
|
||||
createButton: {
|
||||
id: 'course-authoring.create-or-rerun-course.create.button.create',
|
||||
defaultMessage: 'Create',
|
||||
},
|
||||
rerunCreateButton: {
|
||||
id: 'course-authoring.create-or-rerun-course.rerun.button.create',
|
||||
defaultMessage: 'Create re-run',
|
||||
},
|
||||
creatingButton: {
|
||||
id: 'course-authoring.create-or-rerun-course.button.creating',
|
||||
defaultMessage: 'Creating',
|
||||
},
|
||||
rerunningCreateButton: {
|
||||
id: 'course-authoring.create-or-rerun-course.rerun.button.rerunning',
|
||||
defaultMessage: 'Processing re-run request',
|
||||
},
|
||||
cancelButton: {
|
||||
id: 'course-authoring.create-or-rerun-course.button.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
requiredFieldError: {
|
||||
id: 'course-authoring.create-or-rerun-course.required.error',
|
||||
defaultMessage: 'Required field.',
|
||||
},
|
||||
disallowedCharsError: {
|
||||
id: 'course-authoring.create-or-rerun-course.disallowed-chars.error',
|
||||
defaultMessage: 'Please do not use any spaces or special characters in this field.',
|
||||
},
|
||||
noSpaceError: {
|
||||
id: 'course-authoring.create-or-rerun-course.no-space.error',
|
||||
defaultMessage: 'Please do not use any spaces in this field.',
|
||||
},
|
||||
alertErrorExistsAriaLabelledBy: {
|
||||
id: 'course-authoring.create-or-rerun-course.error.already-exists.labelledBy',
|
||||
defaultMessage: 'alert-already-exists-title',
|
||||
},
|
||||
alertErrorExistsAriaDescribedBy: {
|
||||
id: 'course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy',
|
||||
defaultMessage: 'alert-confirmation-description',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
44
src/generic/data/api.js
Normal file
44
src/generic/data/api.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { convertObjectToSnakeCase } from '../../utils';
|
||||
|
||||
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getCreateOrRerunCourseUrl = new URL('course/', getApiBaseUrl()).href;
|
||||
export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href;
|
||||
export const getOrganizationsUrl = new URL('organizations', getApiBaseUrl()).href;
|
||||
|
||||
/**
|
||||
* Get's organizations data.
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getOrganizations() {
|
||||
const { data } = await getAuthenticatedHttpClient().get(
|
||||
getOrganizationsUrl,
|
||||
);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's course rerun data.
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getCourseRerun(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(
|
||||
getCourseRerunUrl(courseId),
|
||||
);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or rerun course with data.
|
||||
* @param {object} data
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function createOrRerunCourse(courseData) {
|
||||
const { data } = await getAuthenticatedHttpClient().post(
|
||||
getCreateOrRerunCourseUrl,
|
||||
convertObjectToSnakeCase(courseData, true),
|
||||
);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
75
src/generic/data/api.test.js
Normal file
75
src/generic/data/api.test.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import {
|
||||
createOrRerunCourse,
|
||||
getApiBaseUrl,
|
||||
getOrganizations,
|
||||
getCreateOrRerunCourseUrl,
|
||||
getCourseRerunUrl,
|
||||
getCourseRerun,
|
||||
} from './api';
|
||||
|
||||
let axiosMock;
|
||||
|
||||
describe('generic api calls', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get organizations', async () => {
|
||||
const organizationsData = ['edX', 'org'];
|
||||
const queryUrl = new URL('organizations', getApiBaseUrl()).href;
|
||||
axiosMock.onGet(queryUrl).reply(200, organizationsData);
|
||||
const result = await getOrganizations();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(queryUrl);
|
||||
expect(result).toEqual(organizationsData);
|
||||
});
|
||||
|
||||
it('should get course rerun', async () => {
|
||||
const courseId = 'course-mock-id';
|
||||
const courseRerunData = {
|
||||
allowUnicodeCourseId: false,
|
||||
courseCreatorStatus: 'granted',
|
||||
displayName: 'Demonstration Course',
|
||||
number: 'DemoX',
|
||||
org: 'edX',
|
||||
run: 'Demo_Course',
|
||||
};
|
||||
axiosMock.onGet(getCourseRerunUrl(courseId)).reply(200, courseRerunData);
|
||||
const result = await getCourseRerun(courseId);
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(getCourseRerunUrl(courseId));
|
||||
expect(result).toEqual(courseRerunData);
|
||||
});
|
||||
|
||||
it('should post create or rerun course', async () => {
|
||||
const courseRerunData = {
|
||||
allowUnicodeCourseId: false,
|
||||
courseCreatorStatus: 'granted',
|
||||
displayName: 'Demonstration Course',
|
||||
number: 'DemoX',
|
||||
org: 'edX',
|
||||
run: 'Demo_Course',
|
||||
};
|
||||
axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, courseRerunData);
|
||||
const result = await createOrRerunCourse(courseRerunData);
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(getCreateOrRerunCourseUrl);
|
||||
expect(result).toEqual(courseRerunData);
|
||||
});
|
||||
});
|
||||
7
src/generic/data/selectors.js
Normal file
7
src/generic/data/selectors.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const getLoadingStatuses = (state) => state.generic.loadingStatuses;
|
||||
export const getSavingStatus = (state) => state.generic.savingStatus;
|
||||
export const getOrganizations = (state) => state.generic.organizations;
|
||||
export const getCourseData = (state) => state.generic.createOrRerunCourse.courseData;
|
||||
export const getCourseRerunData = (state) => state.generic.createOrRerunCourse.courseRerunData;
|
||||
export const getRedirectUrlObj = (state) => state.generic.createOrRerunCourse.redirectUrlObj;
|
||||
export const getPostErrors = (state) => state.generic.createOrRerunCourse.postErrors;
|
||||
59
src/generic/data/slice.js
Normal file
59
src/generic/data/slice.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'generic',
|
||||
initialState: {
|
||||
loadingStatuses: {
|
||||
organizationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseRerunLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
savingStatus: '',
|
||||
organizations: [],
|
||||
createOrRerunCourse: {
|
||||
courseData: {},
|
||||
courseRerunData: {},
|
||||
redirectUrlObj: {},
|
||||
postErrors: {},
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
fetchOrganizations: (state, { payload }) => {
|
||||
state.organizations = payload;
|
||||
},
|
||||
updateLoadingStatuses: (state, { payload }) => {
|
||||
state.loadingStatuses = { ...state.loadingStatuses, ...payload };
|
||||
},
|
||||
updateSavingStatus: (state, { payload }) => {
|
||||
state.savingStatus = payload.status;
|
||||
},
|
||||
updateCourseData: (state, { payload }) => {
|
||||
state.createOrRerunCourse.courseData = payload;
|
||||
},
|
||||
updateCourseRerunData: (state, { payload }) => {
|
||||
state.createOrRerunCourse.courseRerunData = payload;
|
||||
},
|
||||
updateRedirectUrlObj: (state, { payload }) => {
|
||||
state.createOrRerunCourse.redirectUrlObj = payload;
|
||||
},
|
||||
updatePostErrors: (state, { payload }) => {
|
||||
state.createOrRerunCourse.postErrors = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
fetchOrganizations,
|
||||
updatePostErrors,
|
||||
updateCourseRerunData,
|
||||
updateLoadingStatuses,
|
||||
updateSavingStatus,
|
||||
updateCourseData,
|
||||
updateRedirectUrlObj,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
reducer,
|
||||
} = slice;
|
||||
51
src/generic/data/thunks.js
Normal file
51
src/generic/data/thunks.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { createOrRerunCourse, getOrganizations, getCourseRerun } from './api';
|
||||
import {
|
||||
fetchOrganizations,
|
||||
updatePostErrors,
|
||||
updateLoadingStatuses,
|
||||
updateRedirectUrlObj,
|
||||
updateCourseRerunData,
|
||||
updateSavingStatus,
|
||||
} from './slice';
|
||||
|
||||
export function fetchOrganizationsQuery() {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const organizations = await getOrganizations();
|
||||
dispatch(fetchOrganizations(organizations));
|
||||
dispatch(updateLoadingStatuses({ organizationLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ organizationLoadingStatus: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCourseRerunQuery(courseId) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const courseRerun = await getCourseRerun(courseId);
|
||||
dispatch(updateCourseRerunData(courseRerun));
|
||||
dispatch(updateLoadingStatuses({ courseRerunLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ courseRerunLoadingStatus: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCreateOrRerunCourseQuery(courseData) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
|
||||
try {
|
||||
const response = await createOrRerunCourse(courseData);
|
||||
dispatch(updateRedirectUrlObj('url' in response ? response : {}));
|
||||
dispatch(updatePostErrors('errMsg' in response ? response : {}));
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
123
src/generic/help-sidebar/HelpSidebar.jsx
Normal file
123
src/generic/help-sidebar/HelpSidebar.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { otherLinkURLParams } from './constants';
|
||||
import messages from './messages';
|
||||
import HelpSidebarLink from './HelpSidebarLink';
|
||||
|
||||
const HelpSidebar = ({
|
||||
intl,
|
||||
courseId,
|
||||
showOtherSettings,
|
||||
proctoredExamSettingsUrl,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
const {
|
||||
grading,
|
||||
courseTeam,
|
||||
advancedSettings,
|
||||
scheduleAndDetails,
|
||||
groupConfigurations,
|
||||
} = otherLinkURLParams;
|
||||
|
||||
const showOtherLink = (params) => !pathname.includes(params);
|
||||
const generateLegacyURL = (urlParameter) => {
|
||||
const referObj = new URL(`${urlParameter}/${courseId}`, getConfig().STUDIO_BASE_URL);
|
||||
return referObj.href;
|
||||
};
|
||||
|
||||
const scheduleAndDetailsDestination = generateLegacyURL(scheduleAndDetails);
|
||||
const gradingDestination = generateLegacyURL(grading);
|
||||
const courseTeamDestination = generateLegacyURL(courseTeam);
|
||||
const advancedSettingsDestination = generateLegacyURL(advancedSettings);
|
||||
const groupConfigurationsDestination = generateLegacyURL(groupConfigurations);
|
||||
|
||||
return (
|
||||
<aside className={classNames('help-sidebar', className)}>
|
||||
<div className="help-sidebar-about">{children}</div>
|
||||
{showOtherSettings && (
|
||||
<>
|
||||
<hr />
|
||||
<div className="help-sidebar-other">
|
||||
<h4 className="help-sidebar-other-title">
|
||||
{intl.formatMessage(messages.sidebarTitleOther)}
|
||||
</h4>
|
||||
<nav
|
||||
className="help-sidebar-other-links"
|
||||
aria-label={intl.formatMessage(messages.sidebarTitleOther)}
|
||||
>
|
||||
<ul className="p-0 mb-0">
|
||||
{showOtherLink(scheduleAndDetails) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={scheduleAndDetailsDestination}
|
||||
title={intl.formatMessage(
|
||||
messages.sidebarLinkToScheduleAndDetails,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(grading) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={gradingDestination}
|
||||
title={intl.formatMessage(messages.sidebarLinkToGrading)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(courseTeam) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={courseTeamDestination}
|
||||
title={intl.formatMessage(messages.sidebarLinkToCourseTeam)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(groupConfigurations) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={groupConfigurationsDestination}
|
||||
title={intl.formatMessage(
|
||||
messages.sidebarLinkToGroupConfigurations,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(advancedSettings) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={advancedSettingsDestination}
|
||||
title={intl.formatMessage(messages.sidebarLinkToAdvancedSettings)}
|
||||
/>
|
||||
)}
|
||||
{proctoredExamSettingsUrl && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={proctoredExamSettingsUrl}
|
||||
title={intl.formatMessage(
|
||||
messages.sidebarLinkToProctoredExamSettings,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
HelpSidebar.defaultProps = {
|
||||
proctoredExamSettingsUrl: '',
|
||||
className: undefined,
|
||||
courseId: undefined,
|
||||
showOtherSettings: false,
|
||||
};
|
||||
|
||||
HelpSidebar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string,
|
||||
showOtherSettings: PropTypes.bool,
|
||||
proctoredExamSettingsUrl: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(HelpSidebar);
|
||||
@@ -1,6 +1,4 @@
|
||||
.help-sidebar {
|
||||
margin-top: 8.563rem;
|
||||
|
||||
.help-sidebar-about {
|
||||
.help-sidebar-about-title {
|
||||
color: $black;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import HelpSidebar from '.';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import initializeStore from '../../store';
|
||||
import messages from './messages';
|
||||
import { HelpSidebar } from '.';
|
||||
|
||||
const mockPathname = '/foo-bar';
|
||||
|
||||
let store;
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
@@ -15,11 +18,15 @@ jest.mock('react-router-dom', () => ({
|
||||
}));
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<IntlProvider locale="en">
|
||||
<HelpSidebar {...props}>
|
||||
<p>Test children</p>
|
||||
</HelpSidebar>
|
||||
</IntlProvider>
|
||||
<AppProvider store={store} messages={{}}>
|
||||
<IntlProvider locale="en">
|
||||
<HelpSidebar
|
||||
{...props}
|
||||
>
|
||||
<p>Test children</p>
|
||||
</HelpSidebar>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const props = {
|
||||
@@ -29,6 +36,19 @@ const props = {
|
||||
};
|
||||
|
||||
describe('HelpSidebar', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('renders children correctly', () => {
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
expect(getByText('Test children')).toBeTruthy();
|
||||
@@ -57,7 +77,7 @@ describe('HelpSidebar', () => {
|
||||
});
|
||||
|
||||
it('should render proctored mfe url only if passed not empty value', () => {
|
||||
const initialProps = { ...props, proctoredExamSettingsUrl: 'http:/link-to' };
|
||||
const initialProps = { ...props, showOtherSettings: true, proctoredExamSettingsUrl: 'http:/link-to' };
|
||||
const { getByText } = render(<RootWrapper {...initialProps} />);
|
||||
expect(getByText(messages.sidebarLinkToProctoredExamSettings.defaultMessage)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -6,7 +6,11 @@ const HelpSidebarLink = ({ as, pathToPage, title }) => {
|
||||
const TagElement = as;
|
||||
return (
|
||||
<TagElement className="sidebar-link">
|
||||
<Hyperlink destination={pathToPage}>
|
||||
<Hyperlink
|
||||
destination={pathToPage}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{title}
|
||||
</Hyperlink>
|
||||
</TagElement>
|
||||
|
||||
@@ -6,4 +6,5 @@ export const otherLinkURLParams = {
|
||||
advancedSettings: 'settings/advanced',
|
||||
groupConfigurations: 'group_configurations',
|
||||
proctoredExamSettings: 'proctored-exam-settings',
|
||||
studioHome: 'home',
|
||||
};
|
||||
|
||||
2
src/generic/help-sidebar/index.js
Normal file
2
src/generic/help-sidebar/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as HelpSidebar } from './HelpSidebar';
|
||||
export { default as HelpSidebarLink } from './HelpSidebarLink';
|
||||
@@ -1,119 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import HelpSidebarLink from './HelpSidebarLink';
|
||||
import { otherLinkURLParams } from './constants';
|
||||
import messages from './messages';
|
||||
|
||||
const HelpSidebar = ({
|
||||
intl,
|
||||
courseId,
|
||||
showOtherSettings,
|
||||
proctoredExamSettingsUrl,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
const {
|
||||
grading,
|
||||
courseTeam,
|
||||
advancedSettings,
|
||||
scheduleAndDetails,
|
||||
groupConfigurations,
|
||||
} = otherLinkURLParams;
|
||||
|
||||
const showOtherLink = (params) => !pathname.includes(params);
|
||||
const generateLegacyURL = (urlParameter) => {
|
||||
const referObj = new URL(`${urlParameter}/${courseId}`, getConfig().STUDIO_BASE_URL);
|
||||
return referObj.href;
|
||||
};
|
||||
|
||||
const scheduleAndDetailsDestination = generateLegacyURL(scheduleAndDetails);
|
||||
const gradingDestination = generateLegacyURL(grading);
|
||||
const courseTeamDestination = generateLegacyURL(courseTeam);
|
||||
const advancedSettingsDestination = generateLegacyURL(advancedSettings);
|
||||
const groupConfigurationsDestination = generateLegacyURL(groupConfigurations);
|
||||
|
||||
return (
|
||||
<aside className={classNames('help-sidebar', className)}>
|
||||
<div className="help-sidebar-about">{children}</div>
|
||||
<hr />
|
||||
{showOtherSettings && (
|
||||
<div className="help-sidebar-other">
|
||||
<h4 className="help-sidebar-other-title">
|
||||
{intl.formatMessage(messages.sidebarTitleOther)}
|
||||
</h4>
|
||||
<nav
|
||||
className="help-sidebar-other-links"
|
||||
aria-label={intl.formatMessage(messages.sidebarTitleOther)}
|
||||
>
|
||||
<ul className="p-0 mb-0">
|
||||
{showOtherLink(scheduleAndDetails) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={scheduleAndDetailsDestination}
|
||||
title={intl.formatMessage(
|
||||
messages.sidebarLinkToScheduleAndDetails,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(grading) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={gradingDestination}
|
||||
title={intl.formatMessage(messages.sidebarLinkToGrading)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(courseTeam) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={courseTeamDestination}
|
||||
title={intl.formatMessage(messages.sidebarLinkToCourseTeam)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(groupConfigurations) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={groupConfigurationsDestination}
|
||||
title={intl.formatMessage(
|
||||
messages.sidebarLinkToGroupConfigurations,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(advancedSettings) && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={advancedSettingsDestination}
|
||||
title={intl.formatMessage(messages.sidebarLinkToAdvancedSettings)}
|
||||
/>
|
||||
)}
|
||||
{proctoredExamSettingsUrl && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={proctoredExamSettingsUrl}
|
||||
title={intl.formatMessage(
|
||||
messages.sidebarLinkToProctoredExamSettings,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
HelpSidebar.defaultProps = {
|
||||
proctoredExamSettingsUrl: '',
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
HelpSidebar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
showOtherSettings: PropTypes.bool.isRequired,
|
||||
proctoredExamSettingsUrl: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(HelpSidebar);
|
||||
@@ -66,14 +66,15 @@ const InternetConnectionAlert = ({
|
||||
|
||||
InternetConnectionAlert.defaultProps = {
|
||||
isQueryPending: false,
|
||||
onQueryProcessing: null,
|
||||
onQueryProcessing: () => ({}),
|
||||
onInternetConnectionFailed: () => ({}),
|
||||
};
|
||||
|
||||
InternetConnectionAlert.propTypes = {
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
isQueryPending: PropTypes.bool,
|
||||
onQueryProcessing: PropTypes.func,
|
||||
onInternetConnectionFailed: PropTypes.func.isRequired,
|
||||
onInternetConnectionFailed: PropTypes.func,
|
||||
};
|
||||
|
||||
export default InternetConnectionAlert;
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
@import "./sub-header/SubHeader";
|
||||
@import "./section-sub-header/SectionSubHeader";
|
||||
@import "./processing-notification/ProccessingNotification";
|
||||
@import "./create-or-rerun-course/CreateOrRerunCourseForm";
|
||||
@import "./WysiwygEditor";
|
||||
@import "./course-stepper/CouseStepper";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow } from '@edx/paragon';
|
||||
|
||||
const SubHeader = ({
|
||||
title, subtitle, contentTitle, description, instruction, headerActions,
|
||||
@@ -11,9 +12,9 @@ const SubHeader = ({
|
||||
{title}
|
||||
</h2>
|
||||
{headerActions && (
|
||||
<div className="ml-auto sub-header-actions">
|
||||
<ActionRow className="ml-auto sub-header-actions">
|
||||
{headerActions}
|
||||
</div>
|
||||
</ActionRow>
|
||||
)}
|
||||
</header>
|
||||
<header className="sub-header-content">
|
||||
@@ -28,12 +29,14 @@ const SubHeader = ({
|
||||
SubHeader.defaultProps = {
|
||||
instruction: '',
|
||||
description: '',
|
||||
subtitle: '',
|
||||
contentTitle: '',
|
||||
headerActions: null,
|
||||
};
|
||||
SubHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
contentTitle: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
contentTitle: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
instruction: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import HelpSidebar from '../../generic/help-sidebar';
|
||||
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||
import messages from './messages';
|
||||
|
||||
const GradingSidebar = ({ intl, courseId, proctoredExamSettingsUrl }) => (
|
||||
|
||||
@@ -745,5 +745,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -782,5 +782,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -746,5 +746,96 @@
|
||||
"course-authoring.export.description1": "You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.",
|
||||
"course-authoring.export.description2": "Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.",
|
||||
"course-authoring.export.title-under-button": "Export my course content",
|
||||
"course-authoring.export.button.title": "Export course content"
|
||||
"course-authoring.export.button.title": "Export course content",
|
||||
"course-authoring.studio-home.heading.title": "Studio home",
|
||||
"course-authoring.studio-home.add-new-course.btn.text": "New course",
|
||||
"course-authoring.studio-home.courses.tab.title": "Courses",
|
||||
"course-authoring.studio-home.libraries.tab.title": "Libraries",
|
||||
"course-authoring.studio-home.archived.tab.title": "Archived courses",
|
||||
"course-authoring.studio-home.default-section-1.title": "Are you staff on an existing Studio course?",
|
||||
"course-authoring.studio-home.default-section-1.description": "The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.",
|
||||
"course-authoring.studio-home.default-section-2.title": "Create your first course",
|
||||
"course-authoring.studio-home.default-section-2.description": "Your new course is just a click away!",
|
||||
"course-authoring.studio-home.btn.add-new-course.text": "Create your first course",
|
||||
"course-authoring.studio-home.btn.view-live.text": "View live",
|
||||
"course-authoring.studio-home.organization.title": "Organization and library settings",
|
||||
"course-authoring.studio-home.organization.label": "Show all courses in organization:",
|
||||
"course-authoring.studio-home.organization.btn.submit.text": "Submit",
|
||||
"course-authoring.studio-home.btn.re-run.text": "Re-run course",
|
||||
"course-authoring.studio-home.new-course.title": "Create a new course",
|
||||
"course-authoring.studio-home.organization.input.placeholder": "For example, MITx",
|
||||
"course-authoring.studio-home.organization.input.no-options": "No options",
|
||||
"course-authoring.studio-home.collapsible.denied.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.denied.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.denied.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.denied.state": "Denied",
|
||||
"course-authoring.studio-home.collapsible.denied.action.text": "Your request did not meet the criteria/guidelines specified by {platformName} Staff.",
|
||||
"course-authoring.studio-home.collapsible.pending.title": "Your course creator request status",
|
||||
"course-authoring.studio-home.collapsible.pending.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.",
|
||||
"course-authoring.studio-home.collapsible.pending.action.title": "Your course creator request status:",
|
||||
"course-authoring.studio-home.collapsible.pending.state": "Pending",
|
||||
"course-authoring.studio-home.collapsible.pending.action.text": "Your request is currently being reviewed by {platformName} staff and should be updated shortly.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.title": "Becoming a course creator in {studioShortName}",
|
||||
"course-authoring.studio-home.collapsible.unrequested.description": "{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.default": "Request the ability to create courses",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.pending": "Submitting your request",
|
||||
"course-authoring.studio-home.collapsible.unrequested.button.failed": "Sorry, there was an error with your request",
|
||||
"course-authoring.studio-home.sidebar.about.title": "New to {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description": "Click \"Looking for help with Studio\" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.",
|
||||
"course-authoring.studio-home.sidebar.about.getting-started": "Getting started with {studioName}",
|
||||
"course-authoring.studio-home.sidebar.about.header-2": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-2": "In order to create courses in {studioName}, you must {mailTo}",
|
||||
"course-authoring.studio-home.sidebar.about.description-2.mail-to": "contact {platformName} staff to help you create a course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-3": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-3": "In order to create courses in {studioName}, you must have course creator privileges to create your own course.",
|
||||
"course-authoring.studio-home.sidebar.about.header-4": "Can I create courses in {studioName}?",
|
||||
"course-authoring.studio-home.sidebar.about.description-4": "Your request to author courses in {studioName} has been denied. Please {mailTo}.",
|
||||
"course-authoring.studio-home.sidebar.about.description-4.mail-to": "contact {platformName} staff with further questions",
|
||||
"course-authoring.studio-home.processing.title": "Courses being processed",
|
||||
"course-authoring.studio-home.verify-email.heading": "Thanks for signing up, {username}!",
|
||||
"course-authoring.studio-home.verify-email.banner.title": "We need to verify your email address",
|
||||
"course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up, we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.",
|
||||
"course-authoring.studio-home.verify-email.sidebar.title": "Need help?",
|
||||
"course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below.",
|
||||
"course-authoring.studio-home.email-staff.btn.text": "Email staff to create course",
|
||||
"course-authoring.create-or-rerun-course.display-name.label": "Course name",
|
||||
"course-authoring.create-or-rerun-course.display-name.placeholder": "e.g. Introduction to Computer Science",
|
||||
"course-authoring.create-or-rerun-course.create.display-name.help-text": "The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.display-name.help-text": "The public display name for the new course. (This name is often the same as the original course name.)",
|
||||
"course-authoring.create-or-rerun-course.org.label": "Organization",
|
||||
"course-authoring.create-or-rerun-course.org.placeholder": "e.g. UniversityX or OrganizationX",
|
||||
"course-authoring.create-or-rerun-course.create.org.help-text": "The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.",
|
||||
"course-authoring.create-or-rerun-course.rerun.org.help-text": "The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.no-space-allowed.strong": "Note: No spaces or special characters are allowed.",
|
||||
"course-authoring.create-or-rerun-course.org.help-text.strong": "Note: The organization name is part of the course URL.",
|
||||
"course-authoring.create-or-rerun-course.number.label": "Course number",
|
||||
"course-authoring.create-or-rerun-course.number.placeholder": "e.g. CS101",
|
||||
"course-authoring.create-or-rerun-course.create.number.help-text": "The unique number that identifies your course within your organization. {strong}",
|
||||
"course-authoring.create-or-rerun-course.rerun.number.help-text": "The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)",
|
||||
"course-authoring.create-or-rerun-course.number.help-text.strong": "Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.",
|
||||
"course-authoring.create-or-rerun-course.run.label": "Course run",
|
||||
"course-authoring.create-or-rerun-course.run.placeholder": "e.g. 2014_T1",
|
||||
"course-authoring.create-or-rerun-course.create.run.help-text": "The term in which your course will run. {strong}",
|
||||
"course-authoring.create-or-rerun-course.create.rerun.help-text": "The term in which the new course will run. (This value is often different than the original course run value.) {strong}",
|
||||
"course-authoring.create-or-rerun-course.default-placeholder": "Label",
|
||||
"course-authoring.create-or-rerun-course.create.button.create": "Create",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.create": "Create re-run",
|
||||
"course-authoring.create-or-rerun-course.button.creating": "Creating",
|
||||
"course-authoring.create-or-rerun-course.rerun.button.rerunning": "Processing re-run request",
|
||||
"course-authoring.create-or-rerun-course.button.cancel": "Cancel",
|
||||
"course-authoring.create-or-rerun-course.required.error": "Required field.",
|
||||
"course-authoring.create-or-rerun-course.disallowed-chars.error": "Please do not use any spaces or special characters in this field.",
|
||||
"course-authoring.create-or-rerun-course.no-space.error": "Please do not use any spaces in this field.",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.labelledBy": "alert-already-exists-title",
|
||||
"course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy": "alert-confirmation-description",
|
||||
"course-authoring.create-or-rerun-course.org.no-options": "No options",
|
||||
"course-authoring.course-rerun.title": "Create a re-run of a course",
|
||||
"course-authoring.course-rerun.actions.button.cancel": "Cancel",
|
||||
"course-authoring.course-rerun.sidebar.section-1.title": "When will my course re-run start?",
|
||||
"course-authoring.course-rerun.sidebar.section-1.description": "The new course is set to start on January 1, 2030 at midnight (UTC).",
|
||||
"course-authoring.course-rerun.sidebar.section-2.title": "What transfers from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
|
||||
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
|
||||
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
|
||||
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import Placeholder from '@edx/frontend-lib-content-components';
|
||||
import messages from './i18n';
|
||||
|
||||
import initializeStore from './store';
|
||||
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
|
||||
import Head from './head/Head';
|
||||
import { StudioHome } from './studio-home';
|
||||
import CourseRerun from './course-rerun';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import './index.scss';
|
||||
@@ -41,10 +42,7 @@ const App = () => {
|
||||
<Head />
|
||||
<Switch>
|
||||
<Route path="/home">
|
||||
{process.env.ENABLE_NEW_HOME_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
)}
|
||||
<StudioHome />
|
||||
</Route>
|
||||
<Route
|
||||
path="/course/:courseId"
|
||||
@@ -55,6 +53,15 @@ const App = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path="/course_rerun/:courseId"
|
||||
render={({ match }) => {
|
||||
const { params: { courseId } } = match;
|
||||
return (
|
||||
<CourseRerun courseId={courseId} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@import "pages-and-resources/discussions/app-list/AppList";
|
||||
@import "advanced-settings/scss/AdvancedSettings";
|
||||
@import "grading-settings/scss/GradingSettings";
|
||||
@import "studio-home/scss/StudioHome";
|
||||
@import "generic/styles";
|
||||
@import "schedule-and-details/ScheduleAndDetails";
|
||||
@import "pages-and-resources/PagesAndResources";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import HelpSidebar from '../../generic/help-sidebar';
|
||||
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||
import messages from './messages';
|
||||
|
||||
const ScheduleSidebar = ({ courseId, proctoredExamSettingsUrl }) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/
|
||||
import { reducer as customPagesReducer } from './custom-pages/data/slice';
|
||||
import { reducer as advancedSettingsReducer } from './advanced-settings/data/slice';
|
||||
import { reducer as gradingSettingsReducer } from './grading-settings/data/slice';
|
||||
import { reducer as studioHomeReducer } from './studio-home/data/slice';
|
||||
import { reducer as scheduleAndDetailsReducer } from './schedule-and-details/data/slice';
|
||||
import { reducer as liveReducer } from './pages-and-resources/live/data/slice';
|
||||
import { reducer as filesReducer } from './files-and-uploads/data/slice';
|
||||
@@ -15,6 +16,7 @@ import { reducer as CourseUpdatesReducer } from './course-updates/data/slice';
|
||||
import { reducer as processingNotificationReducer } from './generic/processing-notification/data/slice';
|
||||
import { reducer as helpUrlsReducer } from './help-urls/data/slice';
|
||||
import { reducer as courseExportReducer } from './export-page/data/slice';
|
||||
import { reducer as genericReducer } from './generic/data/slice';
|
||||
|
||||
export default function initializeStore(preloadedState = undefined) {
|
||||
return configureStore({
|
||||
@@ -27,6 +29,7 @@ export default function initializeStore(preloadedState = undefined) {
|
||||
scheduleAndDetails: scheduleAndDetailsReducer,
|
||||
advancedSettings: advancedSettingsReducer,
|
||||
gradingSettings: gradingSettingsReducer,
|
||||
studioHome: studioHomeReducer,
|
||||
models: modelsReducer,
|
||||
live: liveReducer,
|
||||
courseTeam: courseTeamReducer,
|
||||
@@ -34,6 +37,7 @@ export default function initializeStore(preloadedState = undefined) {
|
||||
processingNotification: processingNotificationReducer,
|
||||
helpUrls: helpUrlsReducer,
|
||||
courseExport: courseExportReducer,
|
||||
generic: genericReducer,
|
||||
},
|
||||
preloadedState,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext } from 'react';
|
||||
import Responsive from 'react-responsive';
|
||||
@@ -17,7 +15,7 @@ ensureConfig([
|
||||
], 'Header component');
|
||||
|
||||
const Header = ({
|
||||
courseId, courseNumber, courseOrg, courseTitle,
|
||||
courseId, courseNumber, courseOrg, courseTitle, isHiddenMainMenu,
|
||||
}) => {
|
||||
const { authenticatedUser, config } = useContext(AppContext);
|
||||
|
||||
@@ -33,6 +31,7 @@ const Header = ({
|
||||
authenticatedUserAvatar: authenticatedUser?.avatar,
|
||||
studioBaseUrl: config.STUDIO_BASE_URL,
|
||||
logoutUrl: config.LOGOUT_URL,
|
||||
isHiddenMainMenu,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -48,15 +47,18 @@ const Header = ({
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
courseId: PropTypes.string,
|
||||
courseNumber: PropTypes.string,
|
||||
courseOrg: PropTypes.string,
|
||||
courseTitle: PropTypes.string.isRequired,
|
||||
isHiddenMainMenu: PropTypes.bool,
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
courseId: null,
|
||||
courseNumber: null,
|
||||
courseOrg: null,
|
||||
isHiddenMainMenu: false,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
|
||||
@@ -16,25 +15,24 @@ import messages from './messages';
|
||||
|
||||
let store;
|
||||
|
||||
const courseId = 'testEd123';
|
||||
const courseNumber = '123';
|
||||
const courseOrg = 'Ed';
|
||||
const courseTitle = 'test';
|
||||
const RootWrapper = (props) => (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values, react/prop-types
|
||||
<ResponsiveContext.Provider value={{ width: props.screenWidth }}>
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<Header
|
||||
{...props}
|
||||
/>
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
</ResponsiveContext.Provider>
|
||||
);
|
||||
|
||||
const renderComponent = (screenWidth) => {
|
||||
render(
|
||||
<ResponsiveContext.Provider value={{ width: screenWidth }}>
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<Header
|
||||
{...{
|
||||
courseId, courseNumber, courseOrg, courseTitle,
|
||||
}}
|
||||
/>
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
</ResponsiveContext.Provider>,
|
||||
);
|
||||
const props = {
|
||||
courseId: 'testEd123',
|
||||
courseNumber: '123',
|
||||
courseOrg: 'Ed',
|
||||
courseTitle: 'test',
|
||||
};
|
||||
|
||||
describe('Header', () => {
|
||||
@@ -51,39 +49,47 @@ describe('Header', () => {
|
||||
store = initializeStore({});
|
||||
});
|
||||
it('course lock up should be visible', () => {
|
||||
renderComponent(1280);
|
||||
const courseLockUpBlock = screen.getByTestId('course-lock-up-block');
|
||||
const { getByTestId } = render(<RootWrapper screenWidth={1280} {...props} />);
|
||||
const courseLockUpBlock = getByTestId('course-lock-up-block');
|
||||
expect(courseLockUpBlock).toBeVisible();
|
||||
});
|
||||
it('mobile menu should not be visible', () => {
|
||||
renderComponent(1280);
|
||||
const mobileMenuButton = screen.queryByTestId('mobile-menu-button');
|
||||
const { queryByTestId } = render(<RootWrapper screenWidth={1280} {...props} />);
|
||||
const mobileMenuButton = queryByTestId('mobile-menu-button');
|
||||
expect(mobileMenuButton).toBeNull();
|
||||
});
|
||||
it('desktop menu should be visible', () => {
|
||||
renderComponent(1280);
|
||||
const desktopMenu = screen.getByTestId('desktop-menu');
|
||||
const { getByTestId } = render(<RootWrapper screenWidth={1280} {...props} />);
|
||||
const desktopMenu = getByTestId('desktop-menu');
|
||||
expect(desktopMenu).toBeVisible();
|
||||
});
|
||||
it('video uploads should be in content menu', async () => {
|
||||
renderComponent(1280);
|
||||
const contentMenu = screen.getAllByRole('button')[0];
|
||||
const { getAllByRole, getByText } = render(<RootWrapper screenWidth={1280} {...props} />);
|
||||
const contentMenu = getAllByRole('button')[0];
|
||||
await waitFor(() => fireEvent.click(contentMenu));
|
||||
const videoUploadButton = screen.getByText(messages['header.links.videoUploads'].defaultMessage);
|
||||
const videoUploadButton = getByText(messages['header.links.videoUploads'].defaultMessage);
|
||||
expect(videoUploadButton).toBeVisible();
|
||||
});
|
||||
it('maintenance should not be in user menu', async () => {
|
||||
renderComponent(1280);
|
||||
const userMenu = screen.getAllByRole('button')[3];
|
||||
const { getAllByRole, queryByText } = render(<RootWrapper screenWidth={1280} {...props} />);
|
||||
const userMenu = getAllByRole('button')[3];
|
||||
await waitFor(() => fireEvent.click(userMenu));
|
||||
const maintenanceButton = screen.queryByText(messages['header.user.menu.maintenance'].defaultMessage);
|
||||
const maintenanceButton = queryByText(messages['header.user.menu.maintenance'].defaultMessage);
|
||||
expect(maintenanceButton).toBeNull();
|
||||
});
|
||||
it('user menu should use avatar icon', async () => {
|
||||
renderComponent(1280);
|
||||
const avatarIcon = screen.getByTestId('avatar-icon');
|
||||
const { getByTestId } = render(<RootWrapper screenWidth={1280} {...props} />);
|
||||
const avatarIcon = getByTestId('avatar-icon');
|
||||
expect(avatarIcon).toBeVisible();
|
||||
});
|
||||
it('should hide nav items if prop isHiddenMainMenu true', async () => {
|
||||
const initialProps = { ...props, isHiddenMainMenu: true };
|
||||
const { queryByTestId } = render(<RootWrapper screenWidth={1280} {...initialProps} />);
|
||||
const desktopMenu = queryByTestId('desktop-menu');
|
||||
const mobileMenuButton = queryByTestId('mobile-menu-button');
|
||||
expect(mobileMenuButton).toBeNull();
|
||||
expect(desktopMenu).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('mobile', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -99,34 +105,42 @@ describe('Header', () => {
|
||||
store = initializeStore({});
|
||||
});
|
||||
it('course lock up should not be visible', async () => {
|
||||
renderComponent(500);
|
||||
const courseLockUpBlock = screen.queryByTestId('course-lock-up-block');
|
||||
const { queryByTestId } = render(<RootWrapper screenWidth={500} {...props} />);
|
||||
const courseLockUpBlock = queryByTestId('course-lock-up-block');
|
||||
expect(courseLockUpBlock).toBeNull();
|
||||
});
|
||||
it('mobile menu should be visible', async () => {
|
||||
renderComponent(500);
|
||||
const mobileMenuButton = screen.getByTestId('mobile-menu-button');
|
||||
const { getByTestId } = render(<RootWrapper screenWidth={500} {...props} />);
|
||||
const mobileMenuButton = getByTestId('mobile-menu-button');
|
||||
expect(mobileMenuButton).toBeVisible();
|
||||
await waitFor(() => fireEvent.click(mobileMenuButton));
|
||||
const mobileMenu = screen.getByTestId('mobile-menu');
|
||||
const mobileMenu = getByTestId('mobile-menu');
|
||||
expect(mobileMenu).toBeVisible();
|
||||
});
|
||||
it('desktop menu should not be visible', () => {
|
||||
renderComponent(500);
|
||||
const desktopMenu = screen.queryByTestId('desktop-menu');
|
||||
const { queryByTestId } = render(<RootWrapper screenWidth={500} {...props} />);
|
||||
const desktopMenu = queryByTestId('desktop-menu');
|
||||
expect(desktopMenu).toBeNull();
|
||||
});
|
||||
it('maintenance should be in user menu', async () => {
|
||||
renderComponent(500);
|
||||
const userMenu = screen.getAllByRole('button')[1];
|
||||
const { getAllByRole, getByText } = render(<RootWrapper screenWidth={500} {...props} />);
|
||||
const userMenu = getAllByRole('button')[1];
|
||||
await waitFor(() => fireEvent.click(userMenu));
|
||||
const maintenanceButton = screen.getByText(messages['header.user.menu.maintenance'].defaultMessage);
|
||||
const maintenanceButton = getByText(messages['header.user.menu.maintenance'].defaultMessage);
|
||||
expect(maintenanceButton).toBeVisible();
|
||||
});
|
||||
it('user menu should use avatar image', async () => {
|
||||
renderComponent(1280);
|
||||
const avatarImage = screen.getByTestId('avatar-image');
|
||||
const { getByTestId } = render(<RootWrapper screenWidth={500} {...props} />);
|
||||
const avatarImage = getByTestId('avatar-image');
|
||||
expect(avatarImage).toBeVisible();
|
||||
});
|
||||
it('should hide nav items if prop isHiddenMainMenu true', async () => {
|
||||
const initialProps = { ...props, isHiddenMainMenu: true };
|
||||
const { queryByTestId } = render(<RootWrapper screenWidth={500} {...initialProps} />);
|
||||
const desktopMenu = queryByTestId('desktop-menu');
|
||||
const mobileMenuButton = queryByTestId('mobile-menu-button');
|
||||
expect(mobileMenuButton).toBeNull();
|
||||
expect(desktopMenu).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,84 +32,93 @@ const HeaderBody = ({
|
||||
setModalPopupTarget,
|
||||
toggleModalPopup,
|
||||
isModalPopupOpen,
|
||||
isHiddenMainMenu,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
<ActionRow as="header" className="site-header-desktop sticky-top px-4">
|
||||
{isMobile ? (
|
||||
<Button
|
||||
ref={setModalPopupTarget}
|
||||
className="d-inline-flex align-items-center"
|
||||
variant="tertiary"
|
||||
onClick={toggleModalPopup}
|
||||
iconBefore={isModalPopupOpen ? Close : MenuIcon}
|
||||
data-testid="mobile-menu-button"
|
||||
>
|
||||
Menu
|
||||
</Button>
|
||||
) : (
|
||||
<Row className="flex-nowrap m-0">
|
||||
<BrandNav
|
||||
{...{
|
||||
studioBaseUrl,
|
||||
logo,
|
||||
logoAltText,
|
||||
}}
|
||||
/>
|
||||
<CourseLockUp
|
||||
{...{
|
||||
courseId,
|
||||
courseNumber,
|
||||
courseOrg,
|
||||
courseTitle,
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<>
|
||||
<ActionRow.Spacer />
|
||||
<BrandNav
|
||||
{...{
|
||||
studioBaseUrl,
|
||||
logo,
|
||||
logoAltText,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Nav data-testid="desktop-menu">
|
||||
<NavDropdownMenu
|
||||
id={`${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`}
|
||||
buttonTitle={intl.formatMessage(messages['header.links.content'])}
|
||||
items={getContentMenuItem({ studioBaseUrl, courseId, intl })}
|
||||
/>
|
||||
<NavDropdownMenu
|
||||
id={`${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`}
|
||||
buttonTitle={intl.formatMessage(messages['header.links.settings'])}
|
||||
items={getSettingMenuItems({ studioBaseUrl, courseId, intl })}
|
||||
/>
|
||||
<NavDropdownMenu
|
||||
id={`${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`}
|
||||
buttonTitle={intl.formatMessage(messages['header.links.tools'])}
|
||||
items={getToolsMenuItems({ studioBaseUrl, courseId, intl })}
|
||||
/>
|
||||
</Nav>
|
||||
)}
|
||||
<ActionRow.Spacer />
|
||||
<Nav>
|
||||
<UserMenu
|
||||
{...{
|
||||
username,
|
||||
studioBaseUrl,
|
||||
logoutUrl,
|
||||
authenticatedUserAvatar,
|
||||
isAdmin,
|
||||
}}
|
||||
/>
|
||||
</Nav>
|
||||
</ActionRow>
|
||||
);
|
||||
}) => {
|
||||
const renderBrandNav = (
|
||||
<BrandNav
|
||||
{...{
|
||||
studioBaseUrl,
|
||||
logo,
|
||||
logoAltText,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ActionRow as="header" className="site-header-desktop sticky-top px-4 justify-content-start">
|
||||
{isHiddenMainMenu ? (
|
||||
<Row className="flex-nowrap ml-4">
|
||||
{renderBrandNav}
|
||||
</Row>
|
||||
) : (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<Button
|
||||
ref={setModalPopupTarget}
|
||||
className="d-inline-flex align-items-center"
|
||||
variant="tertiary"
|
||||
onClick={toggleModalPopup}
|
||||
iconBefore={isModalPopupOpen ? Close : MenuIcon}
|
||||
data-testid="mobile-menu-button"
|
||||
>
|
||||
Menu
|
||||
</Button>
|
||||
) : (
|
||||
<Row className="flex-nowrap m-0">
|
||||
{renderBrandNav}
|
||||
<CourseLockUp
|
||||
{...{
|
||||
courseId,
|
||||
courseNumber,
|
||||
courseOrg,
|
||||
courseTitle,
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<>
|
||||
<ActionRow.Spacer />
|
||||
{renderBrandNav}
|
||||
</>
|
||||
) : (
|
||||
<Nav data-testid="desktop-menu">
|
||||
<NavDropdownMenu
|
||||
id={`${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`}
|
||||
buttonTitle={intl.formatMessage(messages['header.links.content'])}
|
||||
items={getContentMenuItem({ studioBaseUrl, courseId, intl })}
|
||||
/>
|
||||
<NavDropdownMenu
|
||||
id={`${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`}
|
||||
buttonTitle={intl.formatMessage(messages['header.links.settings'])}
|
||||
items={getSettingMenuItems({ studioBaseUrl, courseId, intl })}
|
||||
/>
|
||||
<NavDropdownMenu
|
||||
id={`${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`}
|
||||
buttonTitle={intl.formatMessage(messages['header.links.tools'])}
|
||||
items={getToolsMenuItems({ studioBaseUrl, courseId, intl })}
|
||||
/>
|
||||
</Nav>
|
||||
)}
|
||||
<ActionRow.Spacer />
|
||||
<Nav>
|
||||
<UserMenu
|
||||
{...{
|
||||
username,
|
||||
studioBaseUrl,
|
||||
logoutUrl,
|
||||
authenticatedUserAvatar,
|
||||
isAdmin,
|
||||
}}
|
||||
/>
|
||||
</Nav>
|
||||
</>
|
||||
)}
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
|
||||
HeaderBody.propTypes = {
|
||||
studioBaseUrl: PropTypes.string.isRequired,
|
||||
@@ -127,6 +136,7 @@ HeaderBody.propTypes = {
|
||||
username: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
isMobile: PropTypes.bool,
|
||||
isHiddenMainMenu: PropTypes.bool,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
@@ -142,6 +152,7 @@ HeaderBody.defaultProps = {
|
||||
username: null,
|
||||
isAdmin: false,
|
||||
isMobile: false,
|
||||
isHiddenMainMenu: false,
|
||||
};
|
||||
|
||||
export default injectIntl(HeaderBody);
|
||||
|
||||
136
src/studio-home/StudioHome.jsx
Normal file
136
src/studio-home/StudioHome.jsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Layout,
|
||||
MailtoLink,
|
||||
} from '@edx/paragon';
|
||||
import { Add as AddIcon } from '@edx/paragon/icons/es5';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Loading from '../generic/Loading';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import Header from '../studio-header/Header';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import AppFooter from '../AppFooter';
|
||||
import HomeSidebar from './home-sidebar';
|
||||
import TabsSection from './tabs-section';
|
||||
import OrganizationSection from './organization-section';
|
||||
import VerifyEmailLayout from './verify-email-layout';
|
||||
import ProcessingCourses from './processing-courses';
|
||||
import CreateNewCourseForm from './create-new-course-form';
|
||||
import messages from './messages';
|
||||
import { useStudioHome } from './hooks';
|
||||
|
||||
const StudioHome = ({ intl }) => {
|
||||
const {
|
||||
isLoadingPage,
|
||||
studioHomeData,
|
||||
isShowProcessing,
|
||||
anyQueryIsFailed,
|
||||
isShowEmailStaff,
|
||||
anyQueryIsPending,
|
||||
showNewCourseContainer,
|
||||
isShowOrganizationDropdown,
|
||||
hasAbilityToCreateNewCourse,
|
||||
setShowNewCourseContainer,
|
||||
} = useStudioHome();
|
||||
|
||||
const {
|
||||
userIsActive,
|
||||
studioShortName,
|
||||
studioRequestEmail,
|
||||
} = studioHomeData;
|
||||
|
||||
if (isLoadingPage) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
function getHeaderButtons() {
|
||||
const headerButtons = [];
|
||||
|
||||
if (isShowEmailStaff) {
|
||||
headerButtons.push(
|
||||
<MailtoLink to={studioRequestEmail}>{intl.formatMessage(messages.emailStaffBtnText)}</MailtoLink>,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasAbilityToCreateNewCourse) {
|
||||
headerButtons.push(
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
iconBefore={AddIcon}
|
||||
size="sm"
|
||||
disabled={showNewCourseContainer}
|
||||
onClick={() => setShowNewCourseContainer(true)}
|
||||
>
|
||||
{intl.formatMessage(messages.addNewCourseBtnText)}
|
||||
</Button>,
|
||||
);
|
||||
}
|
||||
|
||||
return headerButtons;
|
||||
}
|
||||
|
||||
const headerButtons = userIsActive ? getHeaderButtons() : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header isHiddenMainMenu />
|
||||
<Container size="xl" className="studio-home">
|
||||
<section className="mb-4">
|
||||
<article className="studio-home-sub-header">
|
||||
<section>
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle, { studioShortName })}
|
||||
headerActions={headerButtons}
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
{!userIsActive ? (
|
||||
<VerifyEmailLayout />
|
||||
) : (
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
sm={[{ span: 9 }, { span: 3 }]}
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
<section>
|
||||
{showNewCourseContainer && (
|
||||
<CreateNewCourseForm handleOnClickCancel={() => setShowNewCourseContainer(false)} />
|
||||
)}
|
||||
{isShowOrganizationDropdown && <OrganizationSection />}
|
||||
{isShowProcessing && <ProcessingCourses />}
|
||||
<TabsSection
|
||||
tabsData={studioHomeData}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={() => setShowNewCourseContainer(true)}
|
||||
/>
|
||||
</section>
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<HomeSidebar />
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
)}
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
<InternetConnectionAlert
|
||||
isFailed={anyQueryIsFailed}
|
||||
isQueryPending={anyQueryIsPending}
|
||||
/>
|
||||
</div>
|
||||
<AppFooter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StudioHome.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StudioHome);
|
||||
158
src/studio-home/StudioHome.test.jsx
Normal file
158
src/studio-home/StudioHome.test.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
act, fireEvent, render, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { COURSE_CREATOR_STATES } from '../constants';
|
||||
import { executeThunk } from '../utils';
|
||||
import { studioHomeMock } from './__mocks__';
|
||||
import { getStudioHomeApiUrl } from './data/api';
|
||||
import { fetchStudioHomeData } from './data/thunks';
|
||||
import messages from './messages';
|
||||
import createNewCourseMessages from './create-new-course-form/messages';
|
||||
import createOrRerunCourseMessages from '../generic/create-or-rerun-course/messages';
|
||||
import { StudioHome } from '.';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const {
|
||||
studioShortName,
|
||||
studioRequestEmail,
|
||||
} = studioHomeMock;
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<StudioHome intl={injectIntl} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<StudioHome />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
|
||||
it('should render page and page title correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(`${studioShortName} home`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render email staff header button', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.disallowedForThisSite,
|
||||
});
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
expect(getByRole('link', { name: messages.emailStaffBtnText.defaultMessage }))
|
||||
.toHaveAttribute('href', `mailto:${studioRequestEmail}`);
|
||||
});
|
||||
|
||||
it('should render create new course button', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
expect(getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show verify email layout if user inactive', () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
userIsActive: false,
|
||||
});
|
||||
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Thanks for signing up, abc123!', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render create new course container', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
|
||||
const { getByRole, getByText } = render(<RootWrapper />);
|
||||
const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage });
|
||||
|
||||
fireEvent.click(createNewCourseButton);
|
||||
waitFor(() => {
|
||||
expect(getByText(createNewCourseMessages.createNewCourse.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide create new course container', async () => {
|
||||
useSelector.mockReturnValue({
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.granted,
|
||||
});
|
||||
|
||||
const { getByRole, queryByText, getByText } = render(<RootWrapper />);
|
||||
const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage });
|
||||
|
||||
fireEvent.click(createNewCourseButton);
|
||||
waitFor(() => {
|
||||
expect(getByText(createNewCourseMessages.createNewCourse.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cancelButton = getByRole('button', { name: createOrRerunCourseMessages.cancelButton.defaultMessage });
|
||||
fireEvent.click(cancelButton);
|
||||
waitFor(() => {
|
||||
expect(queryByText(createNewCourseMessages.createNewCourse.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show footer', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
|
||||
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
2
src/studio-home/__mocks__/index.js
Normal file
2
src/studio-home/__mocks__/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as studioHomeMock } from './studioHomeMock';
|
||||
77
src/studio-home/__mocks__/studioHomeMock.js
Normal file
77
src/studio-home/__mocks__/studioHomeMock.js
Normal file
@@ -0,0 +1,77 @@
|
||||
module.exports = {
|
||||
activeTab: 'courses',
|
||||
allowCourseReruns: true,
|
||||
allowedOrganizations: ['edx', 'org'],
|
||||
archivedCourses: [
|
||||
{
|
||||
courseKey: 'course-v1:MachineLearning+123+2023',
|
||||
displayName: 'Machine Learning',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:MachineLearning+123+2023/jump_to/block-v1:MachineLearning+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'LSE',
|
||||
rerunLink: '/course_rerun/course-v1:MachineLearning+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:MachineLearning+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'course-v1:Design+123+e.g.2025',
|
||||
displayName: 'Design',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:Design+123+e.g.2025/jump_to/block-v1:Design+123+e.g.2025+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'University of Cape Town',
|
||||
rerunLink: '/course_rerun/course-v1:Design+123+e.g.2025',
|
||||
run: 'e.g.2025',
|
||||
url: '/course/course-v1:Design+123+e.g.2025',
|
||||
},
|
||||
],
|
||||
canCreateOrganizations: true,
|
||||
courseCreatorStatus: 'granted',
|
||||
courses: [
|
||||
{
|
||||
courseKey: 'course-v1:HarvardX+123+2023',
|
||||
displayName: 'Managing Risk in the Information Age',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:HarvardX+123+2023/jump_to/block-v1:HarvardX+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'HarvardX',
|
||||
rerunLink: '/course_rerun/course-v1:HarvardX+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:HarvardX+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'org.0/course_0/Run_0',
|
||||
displayName: 'Run 0',
|
||||
lmsLink: null,
|
||||
number: 'course_0',
|
||||
org: 'org.0',
|
||||
rerunLink: null,
|
||||
run: 'Run_0',
|
||||
url: null,
|
||||
},
|
||||
],
|
||||
inProcessCourseActions: [],
|
||||
libraries: [
|
||||
{
|
||||
displayName: 'MBA',
|
||||
libraryKey: 'library-v1:MBA+123',
|
||||
url: '/library/library-v1:MDA+123',
|
||||
org: 'Cambridge',
|
||||
number: '123',
|
||||
canEdit: true,
|
||||
},
|
||||
],
|
||||
librariesEnabled: true,
|
||||
libraryAuthoringMfeUrl: 'http://somewhere',
|
||||
optimizationEnabled: false,
|
||||
redirectToLibraryAuthoringMfe: false,
|
||||
requestCourseCreatorUrl: '/request_course_creator',
|
||||
rerunCreatorStatus: true,
|
||||
showNewLibraryButton: true,
|
||||
splitStudioHome: false,
|
||||
studioName: 'Studio',
|
||||
studioShortName: 'Studio',
|
||||
studioRequestEmail: 'request@email.com',
|
||||
techSupportEmail: 'technical@example.com',
|
||||
platformName: 'Your Platform Name Here',
|
||||
userIsActive: true,
|
||||
allowToCreateNewOrg: false,
|
||||
};
|
||||
76
src/studio-home/card-item/CardItem.test.jsx
Normal file
76
src/studio-home/card-item/CardItem.test.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from '../messages';
|
||||
import initializeStore from '../../store';
|
||||
import CardItem from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
let store;
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CardItem intl={{ formatMessage: jest.fn() }} {...props} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<CardItem />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
it('should render course details for non-library course', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
expect(getByText(`${props.org} / ${props.number} / ${props.run}`)).toBeInTheDocument();
|
||||
});
|
||||
it('should render correct links for non-library course', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
const courseTitleLink = getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
const btnReRunCourse = getByText(messages.btnReRunText.defaultMessage);
|
||||
expect(btnReRunCourse).toHaveAttribute('href', props.rerunLink);
|
||||
const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage);
|
||||
expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
|
||||
});
|
||||
it('should render course details for library course', () => {
|
||||
const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
|
||||
const { getByText } = render(<RootWrapper {...props} />);
|
||||
const courseTitleLink = getByText(props.displayName);
|
||||
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
|
||||
expect(getByText(`${props.org} / ${props.number}`)).toBeInTheDocument();
|
||||
});
|
||||
it('should hide rerun button if disallowed', () => {
|
||||
const props = studioHomeMock.archivedCourses[0];
|
||||
useSelector.mockReturnValue({ ...studioHomeMock, allowCourseReruns: false });
|
||||
const { queryByText } = render(<RootWrapper {...props} />);
|
||||
expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
it('should be read only course if old mongo course', () => {
|
||||
const props = studioHomeMock.courses[1];
|
||||
const { queryByText } = render(<RootWrapper {...props} />);
|
||||
expect(queryByText(props.displayName)).not.toHaveAttribute('href');
|
||||
expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(queryByText(messages.viewLiveBtnText.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
78
src/studio-home/card-item/index.jsx
Normal file
78
src/studio-home/card-item/index.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, Card, Hyperlink } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../constants';
|
||||
import { getStudioHomeData } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
|
||||
const CardItem = ({
|
||||
intl, displayName, lmsLink, rerunLink, org, number, run, isLibraries, url,
|
||||
}) => {
|
||||
const {
|
||||
allowCourseReruns,
|
||||
courseCreatorStatus,
|
||||
rerunCreatorStatus,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const courseUrl = new URL(url, getConfig().STUDIO_BASE_URL);
|
||||
const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`;
|
||||
const readOnlyItem = !(lmsLink || rerunLink || url);
|
||||
const showActions = !(readOnlyItem || isLibraries);
|
||||
const isShowRerunLink = allowCourseReruns
|
||||
&& rerunCreatorStatus
|
||||
&& courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
|
||||
return (
|
||||
<Card className="card-item">
|
||||
<Card.Header
|
||||
title={!readOnlyItem ? (
|
||||
<Hyperlink
|
||||
className="card-item-title"
|
||||
destination={courseUrl.toString()}
|
||||
>
|
||||
{displayName}
|
||||
</Hyperlink>
|
||||
) : (
|
||||
<span className="card-item-title">{displayName}</span>
|
||||
)}
|
||||
subtitle={subtitle}
|
||||
actions={showActions && (
|
||||
<ActionRow>
|
||||
{isShowRerunLink && (
|
||||
<Hyperlink className="small" destination={rerunLink}>
|
||||
{intl.formatMessage(messages.btnReRunText)}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<Hyperlink className="small ml-3" destination={lmsLink}>
|
||||
{intl.formatMessage(messages.viewLiveBtnText)}
|
||||
</Hyperlink>
|
||||
</ActionRow>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
CardItem.defaultProps = {
|
||||
isLibraries: false,
|
||||
rerunLink: '',
|
||||
lmsLink: '',
|
||||
run: '',
|
||||
};
|
||||
|
||||
CardItem.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
lmsLink: PropTypes.string,
|
||||
rerunLink: PropTypes.string,
|
||||
org: PropTypes.string.isRequired,
|
||||
run: PropTypes.string,
|
||||
number: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
isLibraries: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(CardItem);
|
||||
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../constants';
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { requestCourseCreatorQuery } from '../data/thunks';
|
||||
import { getRequestCourseCreatorUrl } from '../data/api';
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from './messages';
|
||||
import CollapsibleStateWithAction from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
let store; let
|
||||
axiosMock;
|
||||
|
||||
const {
|
||||
studioName,
|
||||
studioShortName,
|
||||
} = studioHomeMock;
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CollapsibleStateWithAction {...props} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const props = {
|
||||
state: COURSE_CREATOR_STATES.unrequested,
|
||||
};
|
||||
|
||||
describe('<CollapsibleStateWithAction />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onPost(getRequestCourseCreatorUrl()).reply(200, studioHomeMock);
|
||||
await executeThunk(requestCourseCreatorQuery(), store.dispatch);
|
||||
});
|
||||
|
||||
it('renders collapsible unrequested state successfully closed', () => {
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
|
||||
const { getByText, queryByText } = render(<RootWrapper {...props} />);
|
||||
expect(getByText(`Becoming a course creator in ${studioShortName}`)).toBeInTheDocument();
|
||||
expect(queryByText(`${studioName} is a hosted solution for our xConsortium partners and selected guests.`)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders collapsible pending state successfully closed', () => {
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
|
||||
const initialState = { ...props, state: COURSE_CREATOR_STATES.pending };
|
||||
const { getByText } = render(<RootWrapper {...initialState} />);
|
||||
expect(getByText(messages.pendingCollapsibleTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders collapsible denied state successfully closed', () => {
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
|
||||
const initialState = { ...props, state: COURSE_CREATOR_STATES.denied };
|
||||
const { getByText } = render(<RootWrapper {...initialState} />);
|
||||
expect(getByText(messages.deniedCollapsibleTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders collapsible denied state successfully opened', () => {
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
|
||||
const initialState = { ...props, state: COURSE_CREATOR_STATES.denied };
|
||||
const { getByText } = render(<RootWrapper {...initialState} />);
|
||||
const container = getByText(messages.deniedCollapsibleTitle.defaultMessage);
|
||||
|
||||
fireEvent.click(container);
|
||||
waitFor(() => {
|
||||
expect(getByText(messages.deniedCollapsibleState.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.deniedCollapsibleActionTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
161
src/studio-home/collapsible-state-with-action/index.jsx
Normal file
161
src/studio-home/collapsible-state-with-action/index.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Collapsible,
|
||||
Bubble,
|
||||
Icon,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Minus as MinusIcon,
|
||||
} from '@edx/paragon/icons/es5';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { COURSE_CREATOR_STATES, STATEFUL_BUTTON_STATES } from '../../constants';
|
||||
import { getStudioHomeData, getSavingStatuses } from '../data/selectors';
|
||||
import { requestCourseCreatorQuery } from '../data/thunks';
|
||||
import messages from './messages';
|
||||
|
||||
const CollapsibleStateWithAction = ({ state, className }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
platformName,
|
||||
studioName,
|
||||
studioShortName,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const { courseCreatorSavingStatus } = useSelector(getSavingStatuses);
|
||||
|
||||
const requestButtonStates = {
|
||||
labels: {
|
||||
default: intl.formatMessage(messages.unrequestedCollapsibleDefaultButton),
|
||||
pending: intl.formatMessage(messages.unrequestedCollapsiblePendingButton),
|
||||
error: intl.formatMessage(messages.unrequestedCollapsibleFailedButton),
|
||||
},
|
||||
disabledStates: [STATEFUL_BUTTON_STATES.pending, STATEFUL_BUTTON_STATES.error],
|
||||
};
|
||||
|
||||
const statusButtonMap = {
|
||||
[RequestStatus.PENDING]: STATEFUL_BUTTON_STATES.pending,
|
||||
[RequestStatus.FAILED]: STATEFUL_BUTTON_STATES.error,
|
||||
};
|
||||
|
||||
const requestButtonCurrentState = statusButtonMap[courseCreatorSavingStatus] || STATEFUL_BUTTON_STATES.default;
|
||||
|
||||
function getTextForStatus() {
|
||||
const matchTextAction = {
|
||||
[COURSE_CREATOR_STATES.denied]: {
|
||||
title: intl.formatMessage(messages.deniedCollapsibleTitle),
|
||||
description: intl.formatMessage(messages.deniedCollapsibleDescription, {
|
||||
studioName,
|
||||
platformName,
|
||||
}),
|
||||
stateName: intl.formatMessage(messages.deniedCollapsibleState),
|
||||
actionTitle: intl.formatMessage(messages.deniedCollapsibleActionTitle),
|
||||
actionText: intl.formatMessage(messages.deniedCollapsibleActionText, {
|
||||
platformName,
|
||||
}),
|
||||
},
|
||||
[COURSE_CREATOR_STATES.unrequested]: {
|
||||
title: intl.formatMessage(messages.unrequestedCollapsibleTitle, {
|
||||
studioShortName,
|
||||
}),
|
||||
description: intl.formatMessage(
|
||||
messages.unrequestedCollapsibleDescription,
|
||||
{ studioName, platformName },
|
||||
),
|
||||
},
|
||||
[COURSE_CREATOR_STATES.pending]: {
|
||||
title: intl.formatMessage(messages.pendingCollapsibleTitle),
|
||||
description: intl.formatMessage(
|
||||
messages.pendingCollapsibleDescription,
|
||||
{ studioName, platformName },
|
||||
),
|
||||
stateName: intl.formatMessage(messages.pendingCollapsibleState),
|
||||
actionTitle: intl.formatMessage(messages.pendingCollapsibleActionTitle),
|
||||
actionText: intl.formatMessage(messages.pendingCollapsibleActionText, {
|
||||
platformName,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return matchTextAction[state];
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
stateName,
|
||||
actionText,
|
||||
description,
|
||||
actionTitle,
|
||||
} = getTextForStatus();
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced
|
||||
className={classNames('collapsible-card rounded-sm', className)}
|
||||
defaultOpen={[COURSE_CREATOR_STATES.denied, COURSE_CREATOR_STATES.pending].includes(state)}
|
||||
>
|
||||
<Collapsible.Trigger className="collapsible-trigger d-flex py-2.5 px-3.5 bg-gray-700">
|
||||
<span className="flex-grow-1 text-white small">{title}</span>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Bubble className="bg-light-700">
|
||||
<Icon
|
||||
src={AddIcon}
|
||||
className="text-gray-700"
|
||||
size="xs"
|
||||
/>
|
||||
</Bubble>
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Bubble className="bg-light-700">
|
||||
<Icon
|
||||
src={MinusIcon}
|
||||
className="text-gray-700"
|
||||
size="xs"
|
||||
/>
|
||||
</Bubble>
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
|
||||
<Collapsible.Body className="collapsible-body bg-light-200 py-3 px-3.5">
|
||||
<p className="small text-gray-700">{description}</p>
|
||||
<h5 className="text-gray-700">{actionTitle}</h5>
|
||||
{[COURSE_CREATOR_STATES.denied, COURSE_CREATOR_STATES.pending].includes(state) ? (
|
||||
<div
|
||||
className={classNames('py-1 px-2.5 rounded-sm', {
|
||||
'bg-danger-300': state === COURSE_CREATOR_STATES.denied,
|
||||
'bg-warning-600': state === COURSE_CREATOR_STATES.pending,
|
||||
})}
|
||||
>
|
||||
<span className="d-inline-block text-white font-weight-bold m-2.5">
|
||||
{stateName}
|
||||
</span>
|
||||
<span className="text-white small">{actionText}</span>
|
||||
</div>
|
||||
) : (
|
||||
<StatefulButton
|
||||
key="request-button"
|
||||
onClick={() => dispatch(requestCourseCreatorQuery())}
|
||||
state={requestButtonCurrentState}
|
||||
{...requestButtonStates}
|
||||
/>
|
||||
)}
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
CollapsibleStateWithAction.defaultProps = {
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
CollapsibleStateWithAction.propTypes = {
|
||||
state: PropTypes.oneOf(Object.values(COURSE_CREATOR_STATES)).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CollapsibleStateWithAction;
|
||||
66
src/studio-home/collapsible-state-with-action/messages.js
Normal file
66
src/studio-home/collapsible-state-with-action/messages.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
deniedCollapsibleTitle: {
|
||||
id: 'course-authoring.studio-home.collapsible.denied.title',
|
||||
defaultMessage: 'Your course creator request status',
|
||||
},
|
||||
deniedCollapsibleDescription: {
|
||||
id: 'course-authoring.studio-home.collapsible.denied.description',
|
||||
defaultMessage: '{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team has completed evaluating your request.',
|
||||
},
|
||||
deniedCollapsibleActionTitle: {
|
||||
id: 'course-authoring.studio-home.collapsible.denied.action.title',
|
||||
defaultMessage: 'Your course creator request status:',
|
||||
},
|
||||
deniedCollapsibleState: {
|
||||
id: 'course-authoring.studio-home.collapsible.denied.state',
|
||||
defaultMessage: 'Denied',
|
||||
},
|
||||
deniedCollapsibleActionText: {
|
||||
id: 'course-authoring.studio-home.collapsible.denied.action.text',
|
||||
defaultMessage: 'Your request did not meet the criteria/guidelines specified by {platformName} Staff.',
|
||||
},
|
||||
pendingCollapsibleTitle: {
|
||||
id: 'course-authoring.studio-home.collapsible.pending.title',
|
||||
defaultMessage: 'Your course creator request status',
|
||||
},
|
||||
pendingCollapsibleDescription: {
|
||||
id: 'course-authoring.studio-home.collapsible.pending.description',
|
||||
defaultMessage: '{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team is currently evaluating your request.',
|
||||
},
|
||||
pendingCollapsibleActionTitle: {
|
||||
id: 'course-authoring.studio-home.collapsible.pending.action.title',
|
||||
defaultMessage: 'Your course creator request status:',
|
||||
},
|
||||
pendingCollapsibleState: {
|
||||
id: 'course-authoring.studio-home.collapsible.pending.state',
|
||||
defaultMessage: 'Pending',
|
||||
},
|
||||
pendingCollapsibleActionText: {
|
||||
id: 'course-authoring.studio-home.collapsible.pending.action.text',
|
||||
defaultMessage: 'Your request is currently being reviewed by {platformName} staff and should be updated shortly.',
|
||||
},
|
||||
unrequestedCollapsibleTitle: {
|
||||
id: 'course-authoring.studio-home.collapsible.unrequested.title',
|
||||
defaultMessage: 'Becoming a course creator in {studioShortName}',
|
||||
},
|
||||
unrequestedCollapsibleDescription: {
|
||||
id: 'course-authoring.studio-home.collapsible.unrequested.description',
|
||||
defaultMessage: '{studioName} is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by {platformName}. Our team will evaluate your request and provide you feedback within 24 hours during the work week.',
|
||||
},
|
||||
unrequestedCollapsibleDefaultButton: {
|
||||
id: 'course-authoring.studio-home.collapsible.unrequested.button.default',
|
||||
defaultMessage: 'Request the ability to create courses',
|
||||
},
|
||||
unrequestedCollapsiblePendingButton: {
|
||||
id: 'course-authoring.studio-home.collapsible.unrequested.button.pending',
|
||||
defaultMessage: 'Submitting your request',
|
||||
},
|
||||
unrequestedCollapsibleFailedButton: {
|
||||
id: 'course-authoring.studio-home.collapsible.unrequested.button.failed',
|
||||
defaultMessage: 'Sorry, there was error with your request',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import initializeStore from '../../store';
|
||||
import messages from './messages';
|
||||
import CourseNewCourseForm from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
let store;
|
||||
|
||||
const onClickCancelMock = jest.fn();
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<CourseNewCourseForm {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const props = {
|
||||
onClickCancel: onClickCancelMock,
|
||||
};
|
||||
|
||||
describe('<CourseNewCourseForm />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
|
||||
it('renders form successfully', () => {
|
||||
const { getByText } = render(
|
||||
<RootWrapper {...props} />,
|
||||
);
|
||||
expect(getByText(messages.createNewCourse.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
33
src/studio-home/create-new-course-form/index.jsx
Normal file
33
src/studio-home/create-new-course-form/index.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { CreateOrRerunCourseForm } from '../../generic/create-or-rerun-course';
|
||||
import messages from './messages';
|
||||
|
||||
const CreateNewCourseForm = ({ handleOnClickCancel }) => {
|
||||
const intl = useIntl();
|
||||
const initialNewCourseData = {
|
||||
displayName: '',
|
||||
org: '',
|
||||
number: '',
|
||||
run: '',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4.5">
|
||||
<CreateOrRerunCourseForm
|
||||
title={intl.formatMessage(messages.createNewCourse)}
|
||||
initialValues={initialNewCourseData}
|
||||
onClickCancel={handleOnClickCancel}
|
||||
isCreateNewCourse
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CreateNewCourseForm.propTypes = {
|
||||
handleOnClickCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CreateNewCourseForm;
|
||||
10
src/studio-home/create-new-course-form/messages.js
Normal file
10
src/studio-home/create-new-course-form/messages.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
createNewCourse: {
|
||||
id: 'course-authoring.studio-home.new-course.title',
|
||||
defaultMessage: 'Create a new course',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
36
src/studio-home/data/api.js
Normal file
36
src/studio-home/data/api.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getStudioHomeApiUrl = (search) => new URL(`api/contentstore/v1/home${search}`, getApiBaseUrl()).href;
|
||||
export const getRequestCourseCreatorUrl = () => new URL('request_course_creator', getApiBaseUrl()).href;
|
||||
export const getCourseNotificationUrl = (url) => new URL(url, getApiBaseUrl()).href;
|
||||
|
||||
/**
|
||||
* Get's studio home data.
|
||||
* @param {string} search
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getStudioHomeData(search) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getStudioHomeApiUrl(search));
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle course notification requests.
|
||||
* @param {string} url
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function handleCourseNotification(url) {
|
||||
const { data } = await getAuthenticatedHttpClient().delete(getCourseNotificationUrl(url));
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send user request to course creation access for studio home data.
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function sendRequestForCourseCreator() {
|
||||
const { data } = await getAuthenticatedHttpClient().post(getRequestCourseCreatorUrl());
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
60
src/studio-home/data/api.test.js
Normal file
60
src/studio-home/data/api.test.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import {
|
||||
getStudioHomeApiUrl,
|
||||
getRequestCourseCreatorUrl,
|
||||
getCourseNotificationUrl,
|
||||
getStudioHomeData,
|
||||
handleCourseNotification,
|
||||
sendRequestForCourseCreator,
|
||||
} from './api';
|
||||
|
||||
let axiosMock;
|
||||
|
||||
describe('studio-home api calls', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get studio home data', async () => {
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||
const result = await getStudioHomeData();
|
||||
|
||||
expect(axiosMock.history.get[0].url).toEqual(getStudioHomeApiUrl());
|
||||
expect(result).toEqual(studioHomeMock);
|
||||
});
|
||||
|
||||
it('should handle course notification request', async () => {
|
||||
const dismissLink = 'to://dismiss-link';
|
||||
const successResponse = { status: 'OK' };
|
||||
axiosMock.onDelete(getCourseNotificationUrl(dismissLink)).reply(200, successResponse);
|
||||
const result = await handleCourseNotification(dismissLink);
|
||||
|
||||
expect(axiosMock.history.delete[0].url).toEqual(getCourseNotificationUrl(dismissLink));
|
||||
expect(result).toEqual(successResponse);
|
||||
});
|
||||
|
||||
it('should send request to course creating access', async () => {
|
||||
const successResponse = { status: 'OK' };
|
||||
axiosMock.onPost(getRequestCourseCreatorUrl()).reply(200, successResponse);
|
||||
const result = await sendRequestForCourseCreator();
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(getRequestCourseCreatorUrl());
|
||||
expect(result).toEqual(successResponse);
|
||||
});
|
||||
});
|
||||
3
src/studio-home/data/selectors.js
Normal file
3
src/studio-home/data/selectors.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const getStudioHomeData = state => state.studioHome.studioHomeData;
|
||||
export const getLoadingStatuses = (state) => state.studioHome.loadingStatuses;
|
||||
export const getSavingStatuses = (state) => state.studioHome.savingStatuses;
|
||||
40
src/studio-home/data/slice.js
Normal file
40
src/studio-home/data/slice.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'studioHome',
|
||||
initialState: {
|
||||
loadingStatuses: {
|
||||
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseNotificationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
savingStatuses: {
|
||||
courseCreatorSavingStatus: '',
|
||||
deleteNotificationSavingStatus: '',
|
||||
},
|
||||
studioHomeData: {},
|
||||
},
|
||||
reducers: {
|
||||
updateLoadingStatuses: (state, { payload }) => {
|
||||
state.loadingStatuses = { ...state.loadingStatuses, ...payload };
|
||||
},
|
||||
updateSavingStatuses: (state, { payload }) => {
|
||||
state.savingStatuses = { ...state.savingStatuses, ...payload };
|
||||
},
|
||||
fetchStudioHomeDataSuccess: (state, { payload }) => {
|
||||
Object.assign(state.studioHomeData, payload);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
updateSavingStatuses,
|
||||
updateLoadingStatuses,
|
||||
fetchStudioHomeDataSuccess,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
reducer,
|
||||
} = slice;
|
||||
55
src/studio-home/data/thunks.js
Normal file
55
src/studio-home/data/thunks.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { getStudioHomeData, sendRequestForCourseCreator, handleCourseNotification } from './api';
|
||||
import {
|
||||
fetchStudioHomeDataSuccess,
|
||||
updateLoadingStatuses,
|
||||
updateSavingStatuses,
|
||||
} from './slice';
|
||||
|
||||
function fetchStudioHomeData(search) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const studioHomeData = await getStudioHomeData(search || '');
|
||||
dispatch(fetchStudioHomeDataSuccess(studioHomeData));
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleDeleteNotificationQuery(url) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatuses({ deleteNotificationSavingStatus: RequestStatus.PENDING }));
|
||||
|
||||
try {
|
||||
await handleCourseNotification(url);
|
||||
dispatch(updateSavingStatuses({ deleteNotificationSavingStatus: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateSavingStatuses({ deleteNotificationSavingStatus: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function requestCourseCreatorQuery() {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: RequestStatus.PENDING }));
|
||||
|
||||
try {
|
||||
await sendRequestForCourseCreator();
|
||||
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
fetchStudioHomeData,
|
||||
requestCourseCreatorQuery,
|
||||
handleDeleteNotificationQuery,
|
||||
};
|
||||
89
src/studio-home/home-sidebar/HomeSidebar.test.jsx
Normal file
89
src/studio-home/home-sidebar/HomeSidebar.test.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../constants';
|
||||
import initializeStore from '../../store';
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import HomeSidebar from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
let store;
|
||||
const {
|
||||
studioName,
|
||||
studioShortName,
|
||||
} = studioHomeMock;
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<HomeSidebar intl={{ formatMessage: jest.fn() }} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<HomeSidebar />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('renders about and other sidebar titles correctly', () => {
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(`New to ${studioName}?`)).toBeInTheDocument();
|
||||
expect(getByText(`Click "Looking for help with Studio" at the bottom of the page to access our continually updated documentation and other ${studioShortName} resources.`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows mail to get instruction', () => {
|
||||
const studioHomeInitial = {
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.disallowedForThisSite,
|
||||
studioRequestEmail: 'mock@example.com',
|
||||
};
|
||||
useSelector.mockReturnValue(studioHomeInitial);
|
||||
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(`Can I create courses in ${studioName}?`)).toBeInTheDocument();
|
||||
expect(getByText(`In order to create courses in ${studioName}, you must`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows unrequested instructions', () => {
|
||||
const studioHomeInitial = {
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.unrequested,
|
||||
};
|
||||
useSelector.mockReturnValue(studioHomeInitial);
|
||||
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(`Can I create courses in ${studioName}?`)).toBeInTheDocument();
|
||||
expect(getByText(`In order to create courses in ${studioName}, you must have course creator privileges to create your own course.`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows denied instructions', () => {
|
||||
const studioHomeInitial = {
|
||||
...studioHomeMock,
|
||||
courseCreatorStatus: COURSE_CREATOR_STATES.denied,
|
||||
};
|
||||
useSelector.mockReturnValue(studioHomeInitial);
|
||||
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(`Can I create courses in ${studioName}?`)).toBeInTheDocument();
|
||||
expect(getByText(`Your request to author courses in ${studioName} has been denied.`, { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
96
src/studio-home/home-sidebar/index.jsx
Normal file
96
src/studio-home/home-sidebar/index.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../constants';
|
||||
import { useHelpUrls } from '../../help-urls/hooks';
|
||||
import { HelpSidebar, HelpSidebarLink } from '../../generic/help-sidebar';
|
||||
import { getStudioHomeData } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const HomeSidebar = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
studioName,
|
||||
platformName,
|
||||
studioShortName,
|
||||
studioRequestEmail,
|
||||
techSupportEmail,
|
||||
courseCreatorStatus,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const { home: aboutHomeLink } = useHelpUrls(['home']);
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const isShowMailToGetInstruction = courseCreatorStatus === COURSE_CREATOR_STATES.disallowedForThisSite
|
||||
&& !!studioRequestEmail;
|
||||
const isShowUnrequestedInstruction = courseCreatorStatus === COURSE_CREATOR_STATES.unrequested;
|
||||
const isShowDeniedInstruction = courseCreatorStatus === COURSE_CREATOR_STATES.denied;
|
||||
|
||||
return (
|
||||
<HelpSidebar>
|
||||
<h4 className="help-sidebar-about-title">
|
||||
{intl.formatMessage(messages.aboutTitle, { studioName })}
|
||||
</h4>
|
||||
<p className="help-sidebar-about-descriptions">
|
||||
{intl.formatMessage(messages.aboutDescription, { studioShortName })}
|
||||
</p>
|
||||
<HelpSidebarLink
|
||||
as="span"
|
||||
pathToPage={aboutHomeLink || ''}
|
||||
title={intl.formatMessage(messages.studioHomeLinkToGettingStarted, { studioName })}
|
||||
/>
|
||||
{isShowMailToGetInstruction && (
|
||||
<>
|
||||
<hr />
|
||||
<h4 className="help-sidebar-about-title">
|
||||
{intl.formatMessage(messages.sidebarHeader2, { studioName })}
|
||||
</h4>
|
||||
<p className="help-sidebar-about-descriptions">
|
||||
{intl.formatMessage(messages.sidebarDescription2, {
|
||||
studioName,
|
||||
mailTo: (
|
||||
<MailtoLink to={studioRequestEmail}>{
|
||||
intl.formatMessage(messages.sidebarDescription2MailTo, { platformName })
|
||||
}
|
||||
</MailtoLink>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{isShowUnrequestedInstruction && (
|
||||
<>
|
||||
<hr />
|
||||
<h4 className="help-sidebar-about-title">
|
||||
{intl.formatMessage(messages.sidebarHeader3, { studioName })}
|
||||
</h4>
|
||||
<p className="help-sidebar-about-descriptions">
|
||||
{intl.formatMessage(messages.sidebarDescription3, { studioName })}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{isShowDeniedInstruction && (
|
||||
<>
|
||||
<hr />
|
||||
<h4 className="help-sidebar-about-title">
|
||||
{intl.formatMessage(messages.sidebarHeader4, { studioName })}
|
||||
</h4>
|
||||
<p className="help-sidebar-about-descriptions">
|
||||
{intl.formatMessage(messages.sidebarDescription4, {
|
||||
studioName,
|
||||
mailTo: (
|
||||
<MailtoLink to={techSupportEmail}>{
|
||||
intl.formatMessage(messages.sidebarDescription4MailTo, { platformName })
|
||||
}
|
||||
</MailtoLink>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</HelpSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeSidebar;
|
||||
50
src/studio-home/home-sidebar/messages.js
Normal file
50
src/studio-home/home-sidebar/messages.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
aboutTitle: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.title',
|
||||
defaultMessage: 'New to {studioName}?',
|
||||
},
|
||||
aboutDescription: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.description',
|
||||
defaultMessage: 'Click "Looking for help with Studio" at the bottom of the page to access our continually updated documentation and other {studioShortName} resources.',
|
||||
},
|
||||
studioHomeLinkToGettingStarted: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.getting-started',
|
||||
defaultMessage: 'Getting started with {studioName}',
|
||||
},
|
||||
sidebarHeader2: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.header-2',
|
||||
defaultMessage: 'Can I create courses in {studioName}?',
|
||||
},
|
||||
sidebarDescription2: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.description-2',
|
||||
defaultMessage: 'In order to create courses in {studioName}, you must {mailTo}',
|
||||
},
|
||||
sidebarDescription2MailTo: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.description-2.mail-to',
|
||||
defaultMessage: 'contact {platformName} staff to help you create a course.',
|
||||
},
|
||||
sidebarHeader3: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.header-3',
|
||||
defaultMessage: 'Can I create courses in {studioName}?',
|
||||
},
|
||||
sidebarDescription3: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.description-3',
|
||||
defaultMessage: 'In order to create courses in {studioName}, you must have course creator privileges to create your own course.',
|
||||
},
|
||||
sidebarHeader4: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.header-4',
|
||||
defaultMessage: 'Can I create courses in {studioName}?',
|
||||
},
|
||||
sidebarDescription4: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.description-4',
|
||||
defaultMessage: 'Your request to author courses in {studioName} has been denied. Please {mailTo}.',
|
||||
},
|
||||
sidebarDescription4MailTo: {
|
||||
id: 'course-authoring.studio-home.sidebar.about.description-4.mail-to',
|
||||
defaultMessage: 'contact {platformName} staff with further questions',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
88
src/studio-home/hooks.jsx
Normal file
88
src/studio-home/hooks.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { COURSE_CREATOR_STATES } from '../constants';
|
||||
import { getCourseData, getSavingStatus } from '../generic/data/selectors';
|
||||
import { fetchStudioHomeData } from './data/thunks';
|
||||
import {
|
||||
getLoadingStatuses,
|
||||
getSavingStatuses,
|
||||
getStudioHomeData,
|
||||
} from './data/selectors';
|
||||
import { updateSavingStatuses } from './data/slice';
|
||||
|
||||
const useStudioHome = () => {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const studioHomeData = useSelector(getStudioHomeData);
|
||||
const newCourseData = useSelector(getCourseData);
|
||||
const { studioHomeLoadingStatus } = useSelector(getLoadingStatuses);
|
||||
const savingCreateRerunStatus = useSelector(getSavingStatus);
|
||||
const {
|
||||
courseCreatorSavingStatus,
|
||||
deleteNotificationSavingStatus,
|
||||
} = useSelector(getSavingStatuses);
|
||||
const [showNewCourseContainer, setShowNewCourseContainer] = useState(false);
|
||||
const isLoadingPage = studioHomeLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStudioHomeData(location.search ?? ''));
|
||||
setShowNewCourseContainer(false);
|
||||
}, [location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
if (courseCreatorSavingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: '' }));
|
||||
dispatch(fetchStudioHomeData());
|
||||
}
|
||||
}, [courseCreatorSavingStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteNotificationSavingStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: '' }));
|
||||
dispatch(fetchStudioHomeData());
|
||||
} else if (deleteNotificationSavingStatus === RequestStatus.FAILED) {
|
||||
dispatch(updateSavingStatuses({ deleteNotificationSavingStatus: '' }));
|
||||
}
|
||||
}, [deleteNotificationSavingStatus]);
|
||||
|
||||
const {
|
||||
allowCourseReruns,
|
||||
rerunCreatorStatus,
|
||||
optimizationEnabled,
|
||||
studioRequestEmail,
|
||||
inProcessCourseActions,
|
||||
courseCreatorStatus,
|
||||
} = studioHomeData;
|
||||
|
||||
const isShowOrganizationDropdown = optimizationEnabled && courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const isShowEmailStaff = courseCreatorStatus === COURSE_CREATOR_STATES.disallowedForThisSite && !!studioRequestEmail;
|
||||
const isShowProcessing = allowCourseReruns && rerunCreatorStatus && inProcessCourseActions.length > 0;
|
||||
const hasAbilityToCreateNewCourse = courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const anyQueryIsPending = [deleteNotificationSavingStatus, courseCreatorSavingStatus, savingCreateRerunStatus]
|
||||
.includes(RequestStatus.PENDING);
|
||||
const anyQueryIsFailed = [deleteNotificationSavingStatus, courseCreatorSavingStatus, savingCreateRerunStatus]
|
||||
.includes(RequestStatus.FAILED);
|
||||
|
||||
return {
|
||||
isLoadingPage,
|
||||
newCourseData,
|
||||
studioHomeData,
|
||||
isShowProcessing,
|
||||
anyQueryIsFailed,
|
||||
isShowEmailStaff,
|
||||
anyQueryIsPending,
|
||||
showNewCourseContainer,
|
||||
courseCreatorSavingStatus,
|
||||
isShowOrganizationDropdown,
|
||||
hasAbilityToCreateNewCourse,
|
||||
deleteNotificationSavingStatus,
|
||||
dispatch,
|
||||
setShowNewCourseContainer,
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useStudioHome };
|
||||
2
src/studio-home/index.js
Normal file
2
src/studio-home/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as StudioHome } from './StudioHome';
|
||||
78
src/studio-home/messages.js
Normal file
78
src/studio-home/messages.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
headingTitle: {
|
||||
id: 'course-authoring.studio-home.heading.title',
|
||||
defaultMessage: '{studioShortName} home',
|
||||
},
|
||||
addNewCourseBtnText: {
|
||||
id: 'course-authoring.studio-home.add-new-course.btn.text',
|
||||
defaultMessage: 'New course',
|
||||
},
|
||||
emailStaffBtnText: {
|
||||
id: 'course-authoring.studio-home.email-staff.btn.text',
|
||||
defaultMessage: 'Email staff to create course',
|
||||
},
|
||||
coursesTabTitle: {
|
||||
id: 'course-authoring.studio-home.courses.tab.title',
|
||||
defaultMessage: 'Courses',
|
||||
},
|
||||
librariesTabTitle: {
|
||||
id: 'course-authoring.studio-home.libraries.tab.title',
|
||||
defaultMessage: 'Libraries',
|
||||
},
|
||||
archivedTabTitle: {
|
||||
id: 'course-authoring.studio-home.archived.tab.title',
|
||||
defaultMessage: 'Archived courses',
|
||||
},
|
||||
defaultSection_1_Title: {
|
||||
id: 'course-authoring.studio-home.default-section-1.title',
|
||||
defaultMessage: 'Are you staff on an existing {studioShortName} course?',
|
||||
},
|
||||
defaultSection_1_Description: {
|
||||
id: 'course-authoring.studio-home.default-section-1.description',
|
||||
defaultMessage: 'The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.',
|
||||
},
|
||||
defaultSection_2_Title: {
|
||||
id: 'course-authoring.studio-home.default-section-2.title',
|
||||
defaultMessage: 'Create your first course',
|
||||
},
|
||||
defaultSection_2_Description: {
|
||||
id: 'course-authoring.studio-home.default-section-2.description',
|
||||
defaultMessage: 'Your new course is just a click away!',
|
||||
},
|
||||
btnAddNewCourseText: {
|
||||
id: 'course-authoring.studio-home.btn.add-new-course.text',
|
||||
defaultMessage: 'Create your first course',
|
||||
},
|
||||
btnReRunText: {
|
||||
id: 'course-authoring.studio-home.btn.re-run.text',
|
||||
defaultMessage: 'Re-run course',
|
||||
},
|
||||
viewLiveBtnText: {
|
||||
id: 'course-authoring.studio-home.btn.view-live.text',
|
||||
defaultMessage: 'View live',
|
||||
},
|
||||
organizationTitle: {
|
||||
id: 'course-authoring.studio-home.organization.title',
|
||||
defaultMessage: 'Organization and library settings',
|
||||
},
|
||||
organizationLabel: {
|
||||
id: 'course-authoring.studio-home.organization.label',
|
||||
defaultMessage: 'Show all courses in organization:',
|
||||
},
|
||||
organizationSubmitBtnText: {
|
||||
id: 'course-authoring.studio-home.organization.btn.submit.text',
|
||||
defaultMessage: 'Submit',
|
||||
},
|
||||
organizationInputPlaceholder: {
|
||||
id: 'course-authoring.studio-home.organization.input.placeholder',
|
||||
defaultMessage: 'For example, MITx',
|
||||
},
|
||||
organizationInputNoOptions: {
|
||||
id: 'course-authoring.studio-home.organization.input.no-options',
|
||||
defaultMessage: 'No options',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { history, initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { fetchOrganizationsQuery } from '../../generic/data/thunks';
|
||||
import { getOrganizationsUrl } from '../../generic/data/api';
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import messages from '../messages';
|
||||
import OrganizationSection from '.';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<OrganizationSection intl={{ formatMessage: jest.fn() }} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<OrganizationSection />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onDelete(getOrganizationsUrl).reply(200);
|
||||
await executeThunk(fetchOrganizationsQuery(), store.dispatch);
|
||||
useSelector.mockReturnValue(['edX', 'org']);
|
||||
});
|
||||
|
||||
it('renders text content correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.organizationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.organizationLabel.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.organizationSubmitBtnText.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should change path after selecting org', () => {
|
||||
const selectedOrgStr = 'edX';
|
||||
const {
|
||||
getByPlaceholderText,
|
||||
getByRole,
|
||||
getByText,
|
||||
getByDisplayValue,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
const orgInput = getByPlaceholderText(messages.organizationInputPlaceholder.defaultMessage);
|
||||
act(() => {
|
||||
fireEvent.click(orgInput);
|
||||
});
|
||||
|
||||
const selectedOrg = getByText(selectedOrgStr);
|
||||
act(() => {
|
||||
fireEvent.click(selectedOrg);
|
||||
});
|
||||
|
||||
const submitButton = getByRole('button', { name: messages.organizationSubmitBtnText.defaultMessage });
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(history.location.pathname).toBe('/home');
|
||||
expect(history.location.search).toBe(`?org=${selectedOrgStr}`);
|
||||
expect(getByDisplayValue(selectedOrgStr)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
77
src/studio-home/organization-section/index.jsx
Normal file
77
src/studio-home/organization-section/index.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { Button, Form, FormLabel } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { TypeaheadDropdown } from '@edx/frontend-lib-content-components';
|
||||
|
||||
import { getOrganizations } from '../../generic/data/selectors';
|
||||
import { fetchOrganizationsQuery } from '../../generic/data/thunks';
|
||||
import messages from '../messages';
|
||||
|
||||
const OrganizationSection = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const fieldName = 'org';
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const orgURLValue = searchParams.get(fieldName) || '';
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const organizations = useSelector(getOrganizations);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(organizations)) {
|
||||
dispatch(fetchOrganizationsQuery());
|
||||
}
|
||||
}, []);
|
||||
|
||||
// We have to set value only after the list of organizations to be received to display the initial state correctly.
|
||||
useEffect(() => {
|
||||
if (organizations.length) {
|
||||
setInputValue(orgURLValue);
|
||||
}
|
||||
}, [orgURLValue, organizations]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
history.push({
|
||||
pathname: '/home',
|
||||
search: `?${fieldName}=${inputValue}`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="organization-section">
|
||||
<h3 className="organization-section-title">
|
||||
{intl.formatMessage(messages.organizationTitle)}
|
||||
</h3>
|
||||
<Form.Group className="organization-section-form d-flex align-items-baseline">
|
||||
<FormLabel className="organization-section-form-label w-50">
|
||||
{intl.formatMessage(messages.organizationLabel)}
|
||||
</FormLabel>
|
||||
<TypeaheadDropdown
|
||||
readOnly={false}
|
||||
name="organizationSearch"
|
||||
value={inputValue}
|
||||
options={organizations}
|
||||
placeholder={intl.formatMessage(messages.organizationInputPlaceholder)}
|
||||
handleBlur={(e) => setInputValue(e.target.value)}
|
||||
handleChange={(value) => setInputValue(value)}
|
||||
noOptionsMessage={intl.formatMessage(messages.organizationInputNoOptions)}
|
||||
helpMessage=""
|
||||
errorMessage=""
|
||||
floatingLabel=""
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button onClick={handleSubmit}>
|
||||
{intl.formatMessage(messages.organizationSubmitBtnText)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
OrganizationSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(OrganizationSection);
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from './messages';
|
||||
import ProcessingCourses from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
let store;
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<ProcessingCourses />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<ProcessingCourses />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
it('renders successfully processing courses', () => {
|
||||
const studioHomeInitial = {
|
||||
...studioHomeMock,
|
||||
inProcessCourseActions: [{ a: '1' }, { b: '2' }],
|
||||
};
|
||||
useSelector.mockReturnValue(studioHomeInitial);
|
||||
const { getByText, queryAllByTestId } = render(<RootWrapper />);
|
||||
expect(getByText(messages.processingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryAllByTestId('course-item')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders successfully empty list', () => {
|
||||
const { getByText, queryAllByTestId } = render(<RootWrapper />);
|
||||
expect(getByText(messages.processingTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryAllByTestId('course-item')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import initializeStore from '../../../store';
|
||||
import { executeThunk } from '../../../utils';
|
||||
import { handleDeleteNotificationQuery } from '../../data/thunks';
|
||||
import { getCourseNotificationUrl } from '../../data/api';
|
||||
import messages from './messages';
|
||||
import CourseItem from '.';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
const RootWrapper = (props) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CourseItem {...props} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const course = {
|
||||
displayName: 'course-name-fake',
|
||||
courseKey: 'course-key-fake',
|
||||
org: 'course-org-fake',
|
||||
number: 'course-number-fake',
|
||||
run: 'course-run-fake',
|
||||
isInProgress: true,
|
||||
isFailed: true,
|
||||
dismissLink: 'course-dismiss-link-fake',
|
||||
};
|
||||
|
||||
const props = { course };
|
||||
|
||||
describe('<CourseItem />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onDelete(getCourseNotificationUrl(course.dismissLink)).reply(200);
|
||||
await executeThunk(handleDeleteNotificationQuery(course.dismissLink), store.dispatch);
|
||||
});
|
||||
|
||||
it('renders successfully', () => {
|
||||
const { getByText, getAllByText } = render(<RootWrapper {...props} />);
|
||||
const subtitle = `${course.org} / ${course.number} / ${course.run}`;
|
||||
expect(getAllByText(course.displayName)).toHaveLength(2);
|
||||
expect(getAllByText(subtitle)).toHaveLength(2);
|
||||
expect(getByText(messages.itemInProgressActionText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.itemIsFailedActionText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.itemFailedFooterText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.itemFailedFooterButton.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
100
src/studio-home/processing-courses/course-item/index.jsx
Normal file
100
src/studio-home/processing-courses/course-item/index.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ActionRow,
|
||||
Card,
|
||||
Hyperlink,
|
||||
Button,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Warning as WarningIcon,
|
||||
RotateRight as RotateRightIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { handleDeleteNotificationQuery } from '../../data/thunks';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseItem = ({ course }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
displayName, org, number, run, isInProgress, isFailed, dismissLink,
|
||||
} = course;
|
||||
const subtitle = `${org} / ${number} / ${run}`;
|
||||
|
||||
return (
|
||||
<div data-testid="course-item">
|
||||
{isInProgress && (
|
||||
<Card className="card-item">
|
||||
<Card.Header
|
||||
title={<p className="card-item-title">{displayName}</p>}
|
||||
subtitle={subtitle}
|
||||
actions={(
|
||||
<ActionRow>
|
||||
<Icon src={RotateRightIcon} className="spinner-icon text-gray-300" />
|
||||
<ActionRow.Spacer />
|
||||
<span className="small text-gray-300">{intl.formatMessage(messages.itemInProgressActionText)}</span>
|
||||
</ActionRow>
|
||||
)}
|
||||
/>
|
||||
<Card.Divider />
|
||||
<Card.Section className="p-3.5 small text-black bg-gray-100">
|
||||
{intl.formatMessage(messages.itemInProgressFooterText, {
|
||||
refresh: (
|
||||
<Hyperlink destination="/home">
|
||||
{intl.formatMessage(messages.itemInProgressFooterHyperlink)}
|
||||
</Hyperlink>
|
||||
),
|
||||
})}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isFailed && (
|
||||
<Card className="card-item">
|
||||
<Card.Header
|
||||
title={<p className="card-item-title">{displayName}</p>}
|
||||
subtitle={subtitle}
|
||||
actions={(
|
||||
<ActionRow>
|
||||
<Icon src={WarningIcon} className="text-danger-300" />
|
||||
<span className="small text-danger-300">{intl.formatMessage(messages.itemIsFailedActionText)}</span>
|
||||
</ActionRow>
|
||||
)}
|
||||
/>
|
||||
<Card.Divider />
|
||||
<Card.Footer className="p-3.5 small text-white bg-danger-300 align-content-between">
|
||||
<span className="w-75 mr-auto">{intl.formatMessage(messages.itemFailedFooterText)}</span>
|
||||
<Button
|
||||
onClick={() => dispatch(handleDeleteNotificationQuery(dismissLink))}
|
||||
iconBefore={CloseIcon}
|
||||
variant="outline-danger"
|
||||
size="sm"
|
||||
>
|
||||
{intl.formatMessage(messages.itemFailedFooterButton)}
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CourseItem.propTypes = {
|
||||
course: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
courseKey: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
number: PropTypes.string.isRequired,
|
||||
run: PropTypes.string.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
isInProgress: PropTypes.bool.isRequired,
|
||||
dismissLink: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default CourseItem;
|
||||
30
src/studio-home/processing-courses/course-item/messages.js
Normal file
30
src/studio-home/processing-courses/course-item/messages.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
itemInProgressFooterText: {
|
||||
id: 'course-authoring.studio-home.processing.course-item.footer.in-progress',
|
||||
defaultMessage: 'The new course will be added to your course list in 5-10 minutes. Return to this page or {refresh} to update the course list. The new course will need some manual configuration.',
|
||||
},
|
||||
itemInProgressFooterHyperlink: {
|
||||
id: 'course-authoring.studio-home.processing.course-item.footer.in-progress.hyperlink',
|
||||
defaultMessage: 'refresh it',
|
||||
},
|
||||
itemInProgressActionText: {
|
||||
id: 'course-authoring.studio-home.processing.course-item.action.in-progress',
|
||||
defaultMessage: 'Configuring as re-run',
|
||||
},
|
||||
itemIsFailedActionText: {
|
||||
id: 'course-authoring.studio-home.processing.course-item.action.failed',
|
||||
defaultMessage: 'Configuration error',
|
||||
},
|
||||
itemFailedFooterText: {
|
||||
id: 'course-authoring.studio-home.processing.course-item.footer.failed',
|
||||
defaultMessage: 'A system error occurred while your course was being processed. Please go to the original course to try the re-run again, or contact your PM for assistance.',
|
||||
},
|
||||
itemFailedFooterButton: {
|
||||
id: 'course-authoring.studio-home.processing.course-item.footer.failed.button',
|
||||
defaultMessage: 'Dismiss',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
32
src/studio-home/processing-courses/index.jsx
Normal file
32
src/studio-home/processing-courses/index.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Stack } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { getStudioHomeData } from '../data/selectors';
|
||||
import CourseItem from './course-item';
|
||||
import messages from './messages';
|
||||
|
||||
const ProcessingCourses = () => {
|
||||
const intl = useIntl();
|
||||
const { inProcessCourseActions } = useSelector(getStudioHomeData);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-gray-300">
|
||||
{intl.formatMessage(messages.processingTitle)}
|
||||
</p>
|
||||
<hr />
|
||||
<Stack gap={3}>
|
||||
{inProcessCourseActions.map((course) => (
|
||||
<CourseItem
|
||||
course={course}
|
||||
key={course.courseKey}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessingCourses;
|
||||
10
src/studio-home/processing-courses/messages.js
Normal file
10
src/studio-home/processing-courses/messages.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
processingTitle: {
|
||||
id: 'course-authoring.studio-home.processing.title',
|
||||
defaultMessage: 'Courses being processed',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
92
src/studio-home/scss/StudioHome.scss
Normal file
92
src/studio-home/scss/StudioHome.scss
Normal file
@@ -0,0 +1,92 @@
|
||||
.studio-home {
|
||||
margin: 3rem 1.5rem 1.5rem;
|
||||
|
||||
.help-sidebar {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.studio-home-sub-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.organization-section {
|
||||
margin-bottom: 2.25rem;
|
||||
|
||||
.organization-section-title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.organization-section-form {
|
||||
margin: $spacer 0;
|
||||
|
||||
.organization-section-form-label {
|
||||
color: $gray-700;
|
||||
margin-bottom: 0;
|
||||
margin-right: .75rem;
|
||||
}
|
||||
|
||||
.organization-section-form-control {
|
||||
border-color: $gray-500;
|
||||
|
||||
.form-control {
|
||||
font-size: .875rem;
|
||||
line-height: 1.5rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.studio-home-tabs {
|
||||
border: none;
|
||||
margin-bottom: 1.625rem;
|
||||
|
||||
.nav-link {
|
||||
border-bottom: .125rem solid $light-400;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.courses-tab {
|
||||
margin: 1.625rem 0;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.pgn__card-header {
|
||||
padding: .9375rem 1.25rem;
|
||||
|
||||
.pgn__card-header-content {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pgn__card-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-item-title {
|
||||
font: normal $font-weight-normal 1.125rem/1.75rem $font-family-base;
|
||||
color: $black;
|
||||
margin-bottom: .1875rem;
|
||||
}
|
||||
|
||||
.pgn__card-header-subtitle-md {
|
||||
font: normal $font-weight-normal .75rem/1.25rem $font-family-base;
|
||||
color: $gray-700;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
animation: rotate 2s infinite linear;
|
||||
}
|
||||
104
src/studio-home/tabs-section/TabsSection.test.jsx
Normal file
104
src/studio-home/tabs-section/TabsSection.test.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { waitFor, render, fireEvent } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { studioHomeMock } from '../__mocks__';
|
||||
import messages from '../messages';
|
||||
import TabsSection from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
const { studioShortName } = studioHomeMock;
|
||||
|
||||
let store;
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TabsSection intl={{ formatMessage: jest.fn() }} tabsData={studioHomeMock} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<TabsSection />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
useSelector.mockReturnValue(studioHomeMock);
|
||||
});
|
||||
it('should render all tabs correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.archivedTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('should render specific course details', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(studioHomeMock.courses[0].displayName)).toBeVisible();
|
||||
expect(getByText(
|
||||
`${studioHomeMock.courses[0].org} / ${studioHomeMock.courses[0].number} / ${studioHomeMock.courses[0].run}`,
|
||||
)).toBeVisible();
|
||||
});
|
||||
it('should switch to Libraries tab and render specific library details', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const librariesTab = getByText(messages.librariesTabTitle.defaultMessage);
|
||||
fireEvent.click(librariesTab);
|
||||
expect(getByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
|
||||
expect(getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible();
|
||||
});
|
||||
it('should switch to Archived tab and render specific archived course details', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const archivedTab = getByText(messages.archivedTabTitle.defaultMessage);
|
||||
fireEvent.click(archivedTab);
|
||||
expect(getByText(studioHomeMock.archivedCourses[0].displayName)).toBeVisible();
|
||||
expect(getByText(
|
||||
`${studioHomeMock.archivedCourses[0].org} / ${studioHomeMock.archivedCourses[0].number} / ${studioHomeMock.archivedCourses[0].run}`,
|
||||
)).toBeVisible();
|
||||
});
|
||||
it('should hide Libraries tab when libraries are disabled', () => {
|
||||
studioHomeMock.librariesEnabled = false;
|
||||
const { queryByText, getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.librariesTabTitle.defaultMessage)).toBeNull();
|
||||
expect(getByText(messages.archivedTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('should hide Archived tab when archived courses are empty', () => {
|
||||
studioHomeMock.librariesEnabled = true;
|
||||
studioHomeMock.archivedCourses = [];
|
||||
const { queryByText, getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.coursesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.librariesTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.archivedTabTitle.defaultMessage)).toBeNull();
|
||||
});
|
||||
it('should render default sections when courses are empty', () => {
|
||||
studioHomeMock.courses = [];
|
||||
const { getByText, getByRole } = render(<RootWrapper />);
|
||||
expect(getByText(`Are you staff on an existing ${studioShortName} course?`)).toBeInTheDocument();
|
||||
expect(getByText(messages.defaultSection_1_Description.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.defaultSection_2_Title.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByText(messages.defaultSection_2_Description.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('should redirect to library authoring mfe', () => {
|
||||
studioHomeMock.redirectToLibraryAuthoringMfe = true;
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const librariesTab = getByText(messages.librariesTabTitle.defaultMessage);
|
||||
fireEvent.click(librariesTab);
|
||||
waitFor(() => {
|
||||
expect(window.location.href).toBe(studioHomeMock.libraryAuthoringMfeUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
41
src/studio-home/tabs-section/archived-tab/index.jsx
Normal file
41
src/studio-home/tabs-section/archived-tab/index.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import CardItem from '../../card-item';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
|
||||
const ArchivedTab = ({ archivedCoursesData }) => (
|
||||
<div className="courses-tab">
|
||||
{sortAlphabeticallyArray(archivedCoursesData).map(({
|
||||
courseKey, displayName, lmsLink, org, rerunLink, number, run, url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
ArchivedTab.propTypes = {
|
||||
archivedCoursesData: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
courseKey: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
lmsLink: PropTypes.string.isRequired,
|
||||
number: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
rerunLink: PropTypes.string.isRequired,
|
||||
run: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default ArchivedTab;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Card } from '@edx/paragon';
|
||||
import { Add as AddIcon } from '@edx/paragon/icons/es5';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { getStudioHomeData } from '../../../data/selectors';
|
||||
import messages from '../../../messages';
|
||||
|
||||
const ContactAdministrator = ({
|
||||
intl, hasAbilityToCreateCourse, showNewCourseContainer, onClickNewCourse,
|
||||
}) => {
|
||||
const { studioShortName } = useSelector(getStudioHomeData);
|
||||
|
||||
return (
|
||||
<Card variant="muted">
|
||||
<Card.Section
|
||||
title={intl.formatMessage(messages.defaultSection_1_Title, { studioShortName })}
|
||||
className="small"
|
||||
>
|
||||
{intl.formatMessage(messages.defaultSection_1_Description)}
|
||||
</Card.Section>
|
||||
{hasAbilityToCreateCourse && (
|
||||
<>
|
||||
<Card.Divider />
|
||||
<Card.Section
|
||||
className="small"
|
||||
title={intl.formatMessage(messages.defaultSection_2_Title)}
|
||||
actions={(
|
||||
<Button
|
||||
iconBefore={AddIcon}
|
||||
variant="outline-primary"
|
||||
disabled={!hasAbilityToCreateCourse || showNewCourseContainer}
|
||||
onClick={onClickNewCourse}
|
||||
>
|
||||
{intl.formatMessage(messages.btnAddNewCourseText)}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(messages.defaultSection_2_Description)}
|
||||
</Card.Section>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
ContactAdministrator.defaultProps = {
|
||||
hasAbilityToCreateCourse: false,
|
||||
};
|
||||
|
||||
ContactAdministrator.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
hasAbilityToCreateCourse: PropTypes.bool,
|
||||
showNewCourseContainer: PropTypes.bool.isRequired,
|
||||
onClickNewCourse: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ContactAdministrator);
|
||||
89
src/studio-home/tabs-section/courses-tab/index.jsx
Normal file
89
src/studio-home/tabs-section/courses-tab/index.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../../constants';
|
||||
import { getStudioHomeData } from '../../data/selectors';
|
||||
import CardItem from '../../card-item';
|
||||
import CollapsibleStateWithAction from '../../collapsible-state-with-action';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
import ContactAdministrator from './contact-administrator';
|
||||
|
||||
const CoursesTab = ({
|
||||
coursesDataItems,
|
||||
showNewCourseContainer,
|
||||
onClickNewCourse,
|
||||
}) => {
|
||||
const {
|
||||
courseCreatorStatus,
|
||||
optimizationEnabled,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const hasAbilityToCreateCourse = courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const showCollapsible = [
|
||||
COURSE_CREATOR_STATES.denied,
|
||||
COURSE_CREATOR_STATES.pending,
|
||||
COURSE_CREATOR_STATES.unrequested,
|
||||
].includes(courseCreatorStatus);
|
||||
|
||||
return (
|
||||
<>
|
||||
{coursesDataItems?.length ? (
|
||||
sortAlphabeticallyArray(coursesDataItems).map(
|
||||
({
|
||||
courseKey,
|
||||
displayName,
|
||||
lmsLink,
|
||||
org,
|
||||
rerunLink,
|
||||
number,
|
||||
run,
|
||||
url,
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
org={org}
|
||||
number={number}
|
||||
run={run}
|
||||
url={url}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (!optimizationEnabled && (
|
||||
<ContactAdministrator
|
||||
hasAbilityToCreateCourse={hasAbilityToCreateCourse}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{showCollapsible && (
|
||||
<CollapsibleStateWithAction
|
||||
state={courseCreatorStatus}
|
||||
className="mt-3"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CoursesTab.propTypes = {
|
||||
coursesDataItems: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
courseKey: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
lmsLink: PropTypes.string.isRequired,
|
||||
number: PropTypes.string.isRequired,
|
||||
org: PropTypes.string.isRequired,
|
||||
rerunLink: PropTypes.string.isRequired,
|
||||
run: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
showNewCourseContainer: PropTypes.bool.isRequired,
|
||||
onClickNewCourse: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CoursesTab;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user