diff --git a/.env.development b/.env.development index b3ccd5f655..8001ff8f56 100644 --- a/.env.development +++ b/.env.development @@ -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 diff --git a/src/AppFooter.jsx b/src/AppFooter.jsx new file mode 100644 index 0000000000..6cb9eb4f1d --- /dev/null +++ b/src/AppFooter.jsx @@ -0,0 +1,18 @@ +import { Footer } from '@edx/frontend-lib-content-components'; + +const AppFooter = () => ( +
+
+); + +export default AppFooter; diff --git a/src/AppFooter.test.jsx b/src/AppFooter.test.jsx new file mode 100644 index 0000000000..b6a3fa2533 --- /dev/null +++ b/src/AppFooter.test.jsx @@ -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('', () => { + const RootWrapper = () => ( + + + + ); + it('should render the footer successfully', () => { + const { getByText } = render(); + expect(getByText('Looking for help with Studio?')).toBeInTheDocument(); + expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL); + }); +}); diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx index dea6503c8e..3c9688c52e 100644 --- a/src/CourseAuthoringPage.jsx +++ b/src/CourseAuthoringPage.jsx @@ -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 = () => ( -
-
-
-); - const CourseAuthoringPage = ({ courseId, children }) => { const dispatch = useDispatch(); diff --git a/src/CourseAuthoringRoutes.test.jsx b/src/CourseAuthoringRoutes.test.jsx index e82963c3af..8e1bd6fd12 100644 --- a/src/CourseAuthoringRoutes.test.jsx +++ b/src/CourseAuthoringRoutes.test.jsx @@ -65,9 +65,7 @@ describe('', () => { 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( @@ -85,9 +83,7 @@ describe('', () => { ); }); - // 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( diff --git a/src/advanced-settings/settings-sidebar/SettingsSidebar.jsx b/src/advanced-settings/settings-sidebar/SettingsSidebar.jsx index 494bac0ded..dd20e63c9d 100644 --- a/src/advanced-settings/settings-sidebar/SettingsSidebar.jsx +++ b/src/advanced-settings/settings-sidebar/SettingsSidebar.jsx @@ -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 }) => ( diff --git a/src/assets/scss/_form.scss b/src/assets/scss/_form.scss index 622513d14c..326bd7a5a5 100644 --- a/src/assets/scss/_form.scss +++ b/src/assets/scss/_form.scss @@ -79,3 +79,14 @@ color: $black; } } + +.dropdown-group-wrapper { + position: relative; + z-index: $zindex-dropdown; + margin-left: auto; + + .dropdown-container { + position: absolute; + width: 100%; + } +} diff --git a/src/constants.js b/src/constants.js index 39ddc22171..6305606e70 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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 = '

 

'; 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', +}; diff --git a/src/course-rerun/CourseRerun.test.jsx b/src/course-rerun/CourseRerun.test.jsx new file mode 100644 index 0000000000..0a85c50d28 --- /dev/null +++ b/src/course-rerun/CourseRerun.test.jsx @@ -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 = () => ( + + + + + + + +); + +describe('', () => { + 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(); + 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(); + 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(); + const spinner = getByRole('status'); + expect(spinner.textContent).toEqual('Loading...'); + }); + }); + + it('should show footer', () => { + const { getByText } = render(); + expect(getByText('Looking for help with Studio?')).toBeInTheDocument(); + expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL); + }); +}); diff --git a/src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx b/src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx new file mode 100644 index 0000000000..5b73f1db3c --- /dev/null +++ b/src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx @@ -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) => ( + + + + + +); + +const props = { + initialFormValues: { + displayName: '', + org: '', + number: '', + run: '', + }, + onClickCancel: onClickCancelMock, +}; + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + useSelector.mockReturnValue(studioHomeMock); + }); + + it('renders description successfully', () => { + const { getByText } = render(); + 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(); + }); +}); diff --git a/src/course-rerun/course-rerun-form/index.jsx b/src/course-rerun/course-rerun-form/index.jsx new file mode 100644 index 0000000000..426166e5ad --- /dev/null +++ b/src/course-rerun/course-rerun-form/index.jsx @@ -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 ( +
+
{intl.formatMessage(messages.rerunCourseDescription, { + strong: ( + {intl.formatMessage(messages.rerunCourseDescriptionStrong)} + ), + })} +
+ +
+ ); +}; + +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; diff --git a/src/course-rerun/course-rerun-form/messages.js b/src/course-rerun/course-rerun-form/messages.js new file mode 100644 index 0000000000..db103fa153 --- /dev/null +++ b/src/course-rerun/course-rerun-form/messages.js @@ -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; diff --git a/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx b/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx new file mode 100644 index 0000000000..ba70ee5f98 --- /dev/null +++ b/src/course-rerun/course-rerun-sidebar/CourseRerunSidebar.test.jsx @@ -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( + + + + + , +); + +describe('', () => { + 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(); + }); +}); diff --git a/src/course-rerun/course-rerun-sidebar/index.jsx b/src/course-rerun/course-rerun-sidebar/index.jsx new file mode 100644 index 0000000000..db5fa6deec --- /dev/null +++ b/src/course-rerun/course-rerun-sidebar/index.jsx @@ -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 ( + + {sidebarMessages.map(({ title, description, link }, index) => { + const isLastSection = index === sidebarMessages.length - 1; + + return ( +
+

{title}

+

{description}

+ {!!link && ( + + {link.text} + + )} + {!isLastSection &&
} +
+ ); + })} +
+ ); +}; + +export default CourseRerunSideBar; diff --git a/src/course-rerun/course-rerun-sidebar/messages.js b/src/course-rerun/course-rerun-sidebar/messages.js new file mode 100644 index 0000000000..b836bf33ab --- /dev/null +++ b/src/course-rerun/course-rerun-sidebar/messages.js @@ -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; diff --git a/src/course-rerun/hooks.jsx b/src/course-rerun/hooks.jsx new file mode 100644 index 0000000000..31f2908c1b --- /dev/null +++ b/src/course-rerun/hooks.jsx @@ -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 }; diff --git a/src/course-rerun/index.jsx b/src/course-rerun/index.jsx new file mode 100644 index 0000000000..32e9988fb6 --- /dev/null +++ b/src/course-rerun/index.jsx @@ -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 ; + } + + const handleRerunCourseCancel = () => { + history.push('/home'); + }; + + return ( + <> +
+ +
+
+
+
+

{intl.formatMessage(messages.rerunTitle)}

+ + + +
+
+ +

{originalCourseData}

+

{displayName}

+
+
+
+
+ + + + + + + + +
+
+
+ +
+ + + ); +}; + +CourseRerun.propTypes = { + courseId: PropTypes.string.isRequired, +}; + +export default CourseRerun; diff --git a/src/course-rerun/messages.js b/src/course-rerun/messages.js new file mode 100644 index 0000000000..8c9c3a5714 --- /dev/null +++ b/src/course-rerun/messages.js @@ -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; diff --git a/src/course-team/course-team-sidebar/CourseTeamSidebar.jsx b/src/course-team/course-team-sidebar/CourseTeamSidebar.jsx index 7da9be0f26..2b7f42c457 100644 --- a/src/course-team/course-team-sidebar/CourseTeamSidebar.jsx +++ b/src/course-team/course-team-sidebar/CourseTeamSidebar.jsx @@ -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 }) => { diff --git a/src/export-page/export-sidebar/ExportSidebar.jsx b/src/export-page/export-sidebar/ExportSidebar.jsx index 0d5f950901..90cfc3326a 100644 --- a/src/export-page/export-sidebar/ExportSidebar.jsx +++ b/src/export-page/export-sidebar/ExportSidebar.jsx @@ -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'; diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx new file mode 100644 index 0000000000..01cc8f5373 --- /dev/null +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx @@ -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: {intl.formatMessage(messages.courseNoteOrgNameIsPartStrong)}, + }) + : intl.formatMessage(messages.courseOrgRerunHelpText, { + strong: ( + <> +
+ + {intl.formatMessage(messages.courseNoteNoSpaceAllowedStrong)} + + + ), + }), + 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: ( + + {intl.formatMessage(messages.courseNotePartCourseURLRequireStrong)} + + ), + }) + : 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: ( + + {intl.formatMessage(messages.courseNotePartCourseURLRequireStrong)} + + ), + }) + : intl.formatMessage(messages.courseRunRerunHelpText, { + strong: ( + <> +
+ + {intl.formatMessage(messages.courseNoteNoSpaceAllowedStrong)} + + + ), + }), + 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 ? ( + setFieldValue(field.name, value)} + noOptionsMessage={intl.formatMessage(messages.courseOrgNoOptions)} + helpMessage="" + errorMessage="" + floatingLabel="" + /> + ) : ( + + + {field.value || intl.formatMessage(messages.courseOrgNoOptions)} + + + {field.options?.map((value) => ( + setFieldValue(field.name, value)} + > + {value} + + ))} + + + )); + + 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 ( +
+ + {showErrorBanner ? ( + +

{title}

+
+ {newCourseFields.map((field) => ( + + {field.label} + {field.name !== 'org' ? ( + + ) : renderOrgField(field)} + {field.helpText} + {hasErrorField(field.name) && ( + + {errors[field.name]} + + )} + + ))} + + + + +
+
+ ); +}; + +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; diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.scss b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.scss new file mode 100644 index 0000000000..5894c96ad2 --- /dev/null +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.scss @@ -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; + } + } +} diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx new file mode 100644 index 0000000000..933c7266c2 --- /dev/null +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx @@ -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) => ( + + + + + +); + +const props = { + title: 'Mocked title', + isCreateNewCourse: true, + initialValues: { + displayName: '', + org: '', + number: '', + run: '', + }, + onClickCancel: onClickCancelMock, +}; + +describe('', 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( + , + ); + 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(); + 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( + , + ); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + expect(getByPlaceholderText(messages.courseOrgPlaceholder.defaultMessage)); + }); + + it('shows button pending state', () => { + useSelector.mockReturnValue(RequestStatus.PENDING); + const { getByRole } = render(); + 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(); + expect(getByText('aaa')).toBeInTheDocument(); + }); + + it('shows error on field', () => { + const { getByPlaceholderText, getByText } = render(); + const numberInput = getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); + + act(() => { + fireEvent.change(numberInput, { target: { value: 'number with invalid (+) symbol' } }); + }); + + waitFor(() => { + expect(getByText(messages.noSpaceError)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/generic/create-or-rerun-course/constants.js b/src/generic/create-or-rerun-course/constants.js new file mode 100644 index 0000000000..85b8bd90a7 --- /dev/null +++ b/src/generic/create-or-rerun-course/constants.js @@ -0,0 +1,4 @@ +const redirectToCourseIndex = (url) => `${url}/outline`; + +// eslint-disable-next-line import/prefer-default-export +export { redirectToCourseIndex }; diff --git a/src/generic/create-or-rerun-course/hooks.jsx b/src/generic/create-or-rerun-course/hooks.jsx new file mode 100644 index 0000000000..179e6f8d23 --- /dev/null +++ b/src/generic/create-or-rerun-course/hooks.jsx @@ -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 }; diff --git a/src/generic/create-or-rerun-course/index.js b/src/generic/create-or-rerun-course/index.js new file mode 100644 index 0000000000..83b3965919 --- /dev/null +++ b/src/generic/create-or-rerun-course/index.js @@ -0,0 +1,2 @@ +export { default as CreateOrRerunCourseForm } from './CreateOrRerunCourseForm'; +export { useCreateOrRerunCourse } from './hooks'; diff --git a/src/generic/create-or-rerun-course/messages.js b/src/generic/create-or-rerun-course/messages.js new file mode 100644 index 0000000000..16a07af20e --- /dev/null +++ b/src/generic/create-or-rerun-course/messages.js @@ -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; diff --git a/src/generic/data/api.js b/src/generic/data/api.js new file mode 100644 index 0000000000..36dce24bd8 --- /dev/null +++ b/src/generic/data/api.js @@ -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} + */ +export async function getOrganizations() { + const { data } = await getAuthenticatedHttpClient().get( + getOrganizationsUrl, + ); + return camelCaseObject(data); +} + +/** + * Get's course rerun data. + * @returns {Promise} + */ +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} + */ +export async function createOrRerunCourse(courseData) { + const { data } = await getAuthenticatedHttpClient().post( + getCreateOrRerunCourseUrl, + convertObjectToSnakeCase(courseData, true), + ); + return camelCaseObject(data); +} diff --git a/src/generic/data/api.test.js b/src/generic/data/api.test.js new file mode 100644 index 0000000000..fd9d561c33 --- /dev/null +++ b/src/generic/data/api.test.js @@ -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); + }); +}); diff --git a/src/generic/data/selectors.js b/src/generic/data/selectors.js new file mode 100644 index 0000000000..461e09fe98 --- /dev/null +++ b/src/generic/data/selectors.js @@ -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; diff --git a/src/generic/data/slice.js b/src/generic/data/slice.js new file mode 100644 index 0000000000..a25112704e --- /dev/null +++ b/src/generic/data/slice.js @@ -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; diff --git a/src/generic/data/thunks.js b/src/generic/data/thunks.js new file mode 100644 index 0000000000..0008a187f4 --- /dev/null +++ b/src/generic/data/thunks.js @@ -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; + } + }; +} diff --git a/src/generic/help-sidebar/HelpSidebar.jsx b/src/generic/help-sidebar/HelpSidebar.jsx new file mode 100644 index 0000000000..0b9cf96bba --- /dev/null +++ b/src/generic/help-sidebar/HelpSidebar.jsx @@ -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 ( + + ); +}; + +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); diff --git a/src/generic/help-sidebar/HelpSidebar.scss b/src/generic/help-sidebar/HelpSidebar.scss index cf37013305..26f08f2e9a 100644 --- a/src/generic/help-sidebar/HelpSidebar.scss +++ b/src/generic/help-sidebar/HelpSidebar.scss @@ -1,6 +1,4 @@ .help-sidebar { - margin-top: 8.563rem; - .help-sidebar-about { .help-sidebar-about-title { color: $black; diff --git a/src/generic/help-sidebar/HelpSidebar.test.jsx b/src/generic/help-sidebar/HelpSidebar.test.jsx index 410ae631b1..fe8b03db26 100644 --- a/src/generic/help-sidebar/HelpSidebar.test.jsx +++ b/src/generic/help-sidebar/HelpSidebar.test.jsx @@ -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) => ( - - -

Test children

-
-
+ + + +

Test children

+
+
+
); 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(); 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(); expect(getByText(messages.sidebarLinkToProctoredExamSettings.defaultMessage)).toBeTruthy(); }); diff --git a/src/generic/help-sidebar/HelpSidebarLink.jsx b/src/generic/help-sidebar/HelpSidebarLink.jsx index 709ef175e7..115e6d85ec 100644 --- a/src/generic/help-sidebar/HelpSidebarLink.jsx +++ b/src/generic/help-sidebar/HelpSidebarLink.jsx @@ -6,7 +6,11 @@ const HelpSidebarLink = ({ as, pathToPage, title }) => { const TagElement = as; return ( - + {title} diff --git a/src/generic/help-sidebar/constants.js b/src/generic/help-sidebar/constants.js index 68ce38a19d..6cc930689d 100644 --- a/src/generic/help-sidebar/constants.js +++ b/src/generic/help-sidebar/constants.js @@ -6,4 +6,5 @@ export const otherLinkURLParams = { advancedSettings: 'settings/advanced', groupConfigurations: 'group_configurations', proctoredExamSettings: 'proctored-exam-settings', + studioHome: 'home', }; diff --git a/src/generic/help-sidebar/index.js b/src/generic/help-sidebar/index.js new file mode 100644 index 0000000000..7f0fc8374a --- /dev/null +++ b/src/generic/help-sidebar/index.js @@ -0,0 +1,2 @@ +export { default as HelpSidebar } from './HelpSidebar'; +export { default as HelpSidebarLink } from './HelpSidebarLink'; diff --git a/src/generic/help-sidebar/index.jsx b/src/generic/help-sidebar/index.jsx deleted file mode 100644 index 5337bb509f..0000000000 --- a/src/generic/help-sidebar/index.jsx +++ /dev/null @@ -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 ( - - ); -}; - -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); diff --git a/src/generic/internet-connection-alert/index.jsx b/src/generic/internet-connection-alert/index.jsx index 6795f13874..23a7ba9157 100644 --- a/src/generic/internet-connection-alert/index.jsx +++ b/src/generic/internet-connection-alert/index.jsx @@ -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; diff --git a/src/generic/styles.scss b/src/generic/styles.scss index a8cacf01d4..becfb9a77a 100644 --- a/src/generic/styles.scss +++ b/src/generic/styles.scss @@ -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"; diff --git a/src/generic/sub-header/SubHeader.jsx b/src/generic/sub-header/SubHeader.jsx index 121326be5f..77d149ec40 100644 --- a/src/generic/sub-header/SubHeader.jsx +++ b/src/generic/sub-header/SubHeader.jsx @@ -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} {headerActions && ( -
+ {headerActions} -
+ )}
@@ -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, diff --git a/src/grading-settings/grading-sidebar/index.jsx b/src/grading-settings/grading-sidebar/index.jsx index 72ee2d5fe4..eff5a602a4 100644 --- a/src/grading-settings/grading-sidebar/index.jsx +++ b/src/grading-settings/grading-sidebar/index.jsx @@ -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 }) => ( diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 978e71536b..c9709852fe 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -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" } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 6c093ff695..e53c017509 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -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" } diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index 31ae13b79d..6b8493323f 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -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" } diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index a2613cbcb1..adfc90d54c 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -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" } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index aaf6a32f4c..98303f4e3c 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -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" } diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index 94e7e32789..e22568d6a7 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -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" } diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 6c093ff695..e53c017509 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -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" } diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 6c093ff695..e53c017509 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -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" } diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index 34311b5f74..b511ac0200 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -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" } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 6c093ff695..e53c017509 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -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" } diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index a1c44f4087..f49865b28a 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -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" } diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index d8ffe39366..5bb50b7a9d 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -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" } diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index 891d1ef162..f811854840 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -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" } diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index 6c093ff695..e53c017509 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -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" } diff --git a/src/index.jsx b/src/index.jsx index e75d9f9640..c07cd8db35 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -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 = () => { - {process.env.ENABLE_NEW_HOME_PAGE === 'true' - && ( - - )} + { ); }} /> + { + const { params: { courseId } } = match; + return ( + + ); + }} + /> ); diff --git a/src/index.scss b/src/index.scss index 69f7b94c19..b80bc11605 100755 --- a/src/index.scss +++ b/src/index.scss @@ -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"; diff --git a/src/schedule-and-details/schedule-sidebar/index.jsx b/src/schedule-and-details/schedule-sidebar/index.jsx index 4feb28c6fa..cd6b79d75a 100644 --- a/src/schedule-and-details/schedule-sidebar/index.jsx +++ b/src/schedule-and-details/schedule-sidebar/index.jsx @@ -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 }) => { diff --git a/src/store.js b/src/store.js index 38a373ade4..d2345a20da 100644 --- a/src/store.js +++ b/src/store.js @@ -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, }); diff --git a/src/studio-header/Header.jsx b/src/studio-header/Header.jsx index e4ef255c7f..1dbe64b373 100644 --- a/src/studio-header/Header.jsx +++ b/src/studio-header/Header.jsx @@ -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; diff --git a/src/studio-header/Header.test.jsx b/src/studio-header/Header.test.jsx index b881d3742f..507f6ab133 100644 --- a/src/studio-header/Header.test.jsx +++ b/src/studio-header/Header.test.jsx @@ -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 + + + +
+ + + +); -const renderComponent = (screenWidth) => { - render( - - - -
- - - , - ); +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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + const desktopMenu = queryByTestId('desktop-menu'); + const mobileMenuButton = queryByTestId('mobile-menu-button'); + expect(mobileMenuButton).toBeNull(); + expect(desktopMenu).toBeNull(); + }); }); }); diff --git a/src/studio-header/HeaderBody.jsx b/src/studio-header/HeaderBody.jsx index e99800ebb7..29faaf168f 100644 --- a/src/studio-header/HeaderBody.jsx +++ b/src/studio-header/HeaderBody.jsx @@ -32,84 +32,93 @@ const HeaderBody = ({ setModalPopupTarget, toggleModalPopup, isModalPopupOpen, + isHiddenMainMenu, // injected intl, -}) => ( - - {isMobile ? ( - - ) : ( - - - - - )} - {isMobile ? ( - <> - - - - ) : ( - - )} - - - -); +}) => { + const renderBrandNav = ( + + ); + + return ( + + {isHiddenMainMenu ? ( + + {renderBrandNav} + + ) : ( + <> + {isMobile ? ( + + ) : ( + + {renderBrandNav} + + + )} + {isMobile ? ( + <> + + {renderBrandNav} + + ) : ( + + )} + + + + )} + + ); +}; 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); diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.jsx new file mode 100644 index 0000000000..b78dbddb16 --- /dev/null +++ b/src/studio-home/StudioHome.jsx @@ -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 ; + } + + function getHeaderButtons() { + const headerButtons = []; + + if (isShowEmailStaff) { + headerButtons.push( + {intl.formatMessage(messages.emailStaffBtnText)}, + ); + } + + if (hasAbilityToCreateNewCourse) { + headerButtons.push( + , + ); + } + + return headerButtons; + } + + const headerButtons = userIsActive ? getHeaderButtons() : []; + + return ( + <> +
+ +
+
+
+ +
+
+ {!userIsActive ? ( + + ) : ( + + +
+ {showNewCourseContainer && ( + setShowNewCourseContainer(false)} /> + )} + {isShowOrganizationDropdown && } + {isShowProcessing && } + setShowNewCourseContainer(true)} + /> +
+
+ + + +
+ )} +
+
+
+ +
+ + + ); +}; + +StudioHome.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(StudioHome); diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx new file mode 100644 index 0000000000..8655698157 --- /dev/null +++ b/src/studio-home/StudioHome.test.jsx @@ -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 = () => ( + + + + + +); + +describe('', 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(); + expect(getByText(`${studioShortName} home`)).toBeInTheDocument(); + }); + + it('should render email staff header button', async () => { + useSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.disallowedForThisSite, + }); + + const { getByRole } = render(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + expect(getByText('Looking for help with Studio?')).toBeInTheDocument(); + expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL); + }); +}); diff --git a/src/studio-home/__mocks__/index.js b/src/studio-home/__mocks__/index.js new file mode 100644 index 0000000000..92461eb0bb --- /dev/null +++ b/src/studio-home/__mocks__/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as studioHomeMock } from './studioHomeMock'; diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js new file mode 100644 index 0000000000..fa466e4cc4 --- /dev/null +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -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, +}; diff --git a/src/studio-home/card-item/CardItem.test.jsx b/src/studio-home/card-item/CardItem.test.jsx new file mode 100644 index 0000000000..71c34f30f0 --- /dev/null +++ b/src/studio-home/card-item/CardItem.test.jsx @@ -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) => ( + + + + + +); + +describe('', () => { + 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(); + 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(); + 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(); + 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(); + 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(); + expect(queryByText(props.displayName)).not.toHaveAttribute('href'); + expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument(); + expect(queryByText(messages.viewLiveBtnText.defaultMessage)).not.toBeInTheDocument(); + }); +}); diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.jsx new file mode 100644 index 0000000000..7591785dec --- /dev/null +++ b/src/studio-home/card-item/index.jsx @@ -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 ( + + + {displayName} + + ) : ( + {displayName} + )} + subtitle={subtitle} + actions={showActions && ( + + {isShowRerunLink && ( + + {intl.formatMessage(messages.btnReRunText)} + + )} + + {intl.formatMessage(messages.viewLiveBtnText)} + + + )} + /> + + ); +}; + +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); diff --git a/src/studio-home/collapsible-state-with-action/CollapsibleStateWithAction.test.jsx b/src/studio-home/collapsible-state-with-action/CollapsibleStateWithAction.test.jsx new file mode 100644 index 0000000000..74c6873672 --- /dev/null +++ b/src/studio-home/collapsible-state-with-action/CollapsibleStateWithAction.test.jsx @@ -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) => ( + + + + + +); + +const props = { + state: COURSE_CREATOR_STATES.unrequested, +}; + +describe('', 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(); + 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(); + 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(); + 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(); + const container = getByText(messages.deniedCollapsibleTitle.defaultMessage); + + fireEvent.click(container); + waitFor(() => { + expect(getByText(messages.deniedCollapsibleState.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.deniedCollapsibleActionTitle.defaultMessage)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/studio-home/collapsible-state-with-action/index.jsx b/src/studio-home/collapsible-state-with-action/index.jsx new file mode 100644 index 0000000000..551c5571a3 --- /dev/null +++ b/src/studio-home/collapsible-state-with-action/index.jsx @@ -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 ( + + + {title} + + + + + + + + + + + + + +

{description}

+
{actionTitle}
+ {[COURSE_CREATOR_STATES.denied, COURSE_CREATOR_STATES.pending].includes(state) ? ( +
+ + {stateName} + + {actionText} +
+ ) : ( + dispatch(requestCourseCreatorQuery())} + state={requestButtonCurrentState} + {...requestButtonStates} + /> + )} +
+
+ ); +}; + +CollapsibleStateWithAction.defaultProps = { + className: undefined, +}; + +CollapsibleStateWithAction.propTypes = { + state: PropTypes.oneOf(Object.values(COURSE_CREATOR_STATES)).isRequired, + className: PropTypes.string, +}; + +export default CollapsibleStateWithAction; diff --git a/src/studio-home/collapsible-state-with-action/messages.js b/src/studio-home/collapsible-state-with-action/messages.js new file mode 100644 index 0000000000..7dff14be44 --- /dev/null +++ b/src/studio-home/collapsible-state-with-action/messages.js @@ -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; diff --git a/src/studio-home/create-new-course-form/CourseNewCourseForm.test.jsx b/src/studio-home/create-new-course-form/CourseNewCourseForm.test.jsx new file mode 100644 index 0000000000..26105c39ab --- /dev/null +++ b/src/studio-home/create-new-course-form/CourseNewCourseForm.test.jsx @@ -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) => ( + + + + + +); + +const props = { + onClickCancel: onClickCancelMock, +}; + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + useSelector.mockReturnValue(studioHomeMock); + }); + + it('renders form successfully', () => { + const { getByText } = render( + , + ); + expect(getByText(messages.createNewCourse.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/studio-home/create-new-course-form/index.jsx b/src/studio-home/create-new-course-form/index.jsx new file mode 100644 index 0000000000..f0b255bfe0 --- /dev/null +++ b/src/studio-home/create-new-course-form/index.jsx @@ -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 ( +
+ +
+ ); +}; + +CreateNewCourseForm.propTypes = { + handleOnClickCancel: PropTypes.func.isRequired, +}; + +export default CreateNewCourseForm; diff --git a/src/studio-home/create-new-course-form/messages.js b/src/studio-home/create-new-course-form/messages.js new file mode 100644 index 0000000000..41e2322eeb --- /dev/null +++ b/src/studio-home/create-new-course-form/messages.js @@ -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; diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js new file mode 100644 index 0000000000..f6035b9234 --- /dev/null +++ b/src/studio-home/data/api.js @@ -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} + */ +export async function getStudioHomeData(search) { + const { data } = await getAuthenticatedHttpClient().get(getStudioHomeApiUrl(search)); + return camelCaseObject(data); +} + +/** + * Handle course notification requests. + * @param {string} url + * @returns {Promise} +*/ +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} + */ +export async function sendRequestForCourseCreator() { + const { data } = await getAuthenticatedHttpClient().post(getRequestCourseCreatorUrl()); + return camelCaseObject(data); +} diff --git a/src/studio-home/data/api.test.js b/src/studio-home/data/api.test.js new file mode 100644 index 0000000000..f608ed906e --- /dev/null +++ b/src/studio-home/data/api.test.js @@ -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); + }); +}); diff --git a/src/studio-home/data/selectors.js b/src/studio-home/data/selectors.js new file mode 100644 index 0000000000..62957e58af --- /dev/null +++ b/src/studio-home/data/selectors.js @@ -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; diff --git a/src/studio-home/data/slice.js b/src/studio-home/data/slice.js new file mode 100644 index 0000000000..0b35c7ede9 --- /dev/null +++ b/src/studio-home/data/slice.js @@ -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; diff --git a/src/studio-home/data/thunks.js b/src/studio-home/data/thunks.js new file mode 100644 index 0000000000..95815b7d7f --- /dev/null +++ b/src/studio-home/data/thunks.js @@ -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, +}; diff --git a/src/studio-home/home-sidebar/HomeSidebar.test.jsx b/src/studio-home/home-sidebar/HomeSidebar.test.jsx new file mode 100644 index 0000000000..38a506dd58 --- /dev/null +++ b/src/studio-home/home-sidebar/HomeSidebar.test.jsx @@ -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 = () => ( + + + + + +); + +describe('', () => { + 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(); + 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(); + 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(); + 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(); + expect(getByText(`Can I create courses in ${studioName}?`)).toBeInTheDocument(); + expect(getByText(`Your request to author courses in ${studioName} has been denied.`, { exact: false })).toBeInTheDocument(); + }); +}); diff --git a/src/studio-home/home-sidebar/index.jsx b/src/studio-home/home-sidebar/index.jsx new file mode 100644 index 0000000000..af470b92c7 --- /dev/null +++ b/src/studio-home/home-sidebar/index.jsx @@ -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 ( + +

+ {intl.formatMessage(messages.aboutTitle, { studioName })} +

+

+ {intl.formatMessage(messages.aboutDescription, { studioShortName })} +

+ + {isShowMailToGetInstruction && ( + <> +
+

+ {intl.formatMessage(messages.sidebarHeader2, { studioName })} +

+

+ {intl.formatMessage(messages.sidebarDescription2, { + studioName, + mailTo: ( + { + intl.formatMessage(messages.sidebarDescription2MailTo, { platformName }) + } + + ), + })} +

+ + )} + {isShowUnrequestedInstruction && ( + <> +
+

+ {intl.formatMessage(messages.sidebarHeader3, { studioName })} +

+

+ {intl.formatMessage(messages.sidebarDescription3, { studioName })} +

+ + )} + {isShowDeniedInstruction && ( + <> +
+

+ {intl.formatMessage(messages.sidebarHeader4, { studioName })} +

+

+ {intl.formatMessage(messages.sidebarDescription4, { + studioName, + mailTo: ( + { + intl.formatMessage(messages.sidebarDescription4MailTo, { platformName }) + } + + ), + })} +

+ + )} +
+ ); +}; + +export default HomeSidebar; diff --git a/src/studio-home/home-sidebar/messages.js b/src/studio-home/home-sidebar/messages.js new file mode 100644 index 0000000000..161ea021a1 --- /dev/null +++ b/src/studio-home/home-sidebar/messages.js @@ -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; diff --git a/src/studio-home/hooks.jsx b/src/studio-home/hooks.jsx new file mode 100644 index 0000000000..c351d26ae1 --- /dev/null +++ b/src/studio-home/hooks.jsx @@ -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 }; diff --git a/src/studio-home/index.js b/src/studio-home/index.js new file mode 100644 index 0000000000..9e496c5cff --- /dev/null +++ b/src/studio-home/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as StudioHome } from './StudioHome'; diff --git a/src/studio-home/messages.js b/src/studio-home/messages.js new file mode 100644 index 0000000000..d39ba522c0 --- /dev/null +++ b/src/studio-home/messages.js @@ -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; diff --git a/src/studio-home/organization-section/OrganizationSection.test.jsx b/src/studio-home/organization-section/OrganizationSection.test.jsx new file mode 100644 index 0000000000..9589335e52 --- /dev/null +++ b/src/studio-home/organization-section/OrganizationSection.test.jsx @@ -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 = () => ( + + + + + +); + +describe('', 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(); + 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(); + + 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(); + }); +}); diff --git a/src/studio-home/organization-section/index.jsx b/src/studio-home/organization-section/index.jsx new file mode 100644 index 0000000000..c6711efe37 --- /dev/null +++ b/src/studio-home/organization-section/index.jsx @@ -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 ( +
+

+ {intl.formatMessage(messages.organizationTitle)} +

+ + + {intl.formatMessage(messages.organizationLabel)} + + setInputValue(e.target.value)} + handleChange={(value) => setInputValue(value)} + noOptionsMessage={intl.formatMessage(messages.organizationInputNoOptions)} + helpMessage="" + errorMessage="" + floatingLabel="" + /> + + +
+ ); +}; + +OrganizationSection.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(OrganizationSection); diff --git a/src/studio-home/processing-courses/ProcessingCourses.test.jsx b/src/studio-home/processing-courses/ProcessingCourses.test.jsx new file mode 100644 index 0000000000..c9f2595685 --- /dev/null +++ b/src/studio-home/processing-courses/ProcessingCourses.test.jsx @@ -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 = () => ( + + + + + +); + +describe('', () => { + 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(); + expect(getByText(messages.processingTitle.defaultMessage)).toBeInTheDocument(); + expect(queryAllByTestId('course-item')).toHaveLength(2); + }); + + it('renders successfully empty list', () => { + const { getByText, queryAllByTestId } = render(); + expect(getByText(messages.processingTitle.defaultMessage)).toBeInTheDocument(); + expect(queryAllByTestId('course-item')).toHaveLength(0); + }); +}); diff --git a/src/studio-home/processing-courses/course-item/CourseItem.test.jsx b/src/studio-home/processing-courses/course-item/CourseItem.test.jsx new file mode 100644 index 0000000000..3c55e36fc0 --- /dev/null +++ b/src/studio-home/processing-courses/course-item/CourseItem.test.jsx @@ -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) => ( + + + + + +); + +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('', 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(); + 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(); + }); +}); diff --git a/src/studio-home/processing-courses/course-item/index.jsx b/src/studio-home/processing-courses/course-item/index.jsx new file mode 100644 index 0000000000..32f76239ce --- /dev/null +++ b/src/studio-home/processing-courses/course-item/index.jsx @@ -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 ( +
+ {isInProgress && ( + + {displayName}

} + subtitle={subtitle} + actions={( + + + + {intl.formatMessage(messages.itemInProgressActionText)} + + )} + /> + + + {intl.formatMessage(messages.itemInProgressFooterText, { + refresh: ( + + {intl.formatMessage(messages.itemInProgressFooterHyperlink)} + + ), + })} + +
+ )} + + {isFailed && ( + + {displayName}

} + subtitle={subtitle} + actions={( + + + {intl.formatMessage(messages.itemIsFailedActionText)} + + )} + /> + + + {intl.formatMessage(messages.itemFailedFooterText)} + + +
+ )} +
+ ); +}; + +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; diff --git a/src/studio-home/processing-courses/course-item/messages.js b/src/studio-home/processing-courses/course-item/messages.js new file mode 100644 index 0000000000..3543d5af25 --- /dev/null +++ b/src/studio-home/processing-courses/course-item/messages.js @@ -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; diff --git a/src/studio-home/processing-courses/index.jsx b/src/studio-home/processing-courses/index.jsx new file mode 100644 index 0000000000..b41786fd03 --- /dev/null +++ b/src/studio-home/processing-courses/index.jsx @@ -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 ( + <> +

+ {intl.formatMessage(messages.processingTitle)} +

+
+ + {inProcessCourseActions.map((course) => ( + + ))} + + + ); +}; + +export default ProcessingCourses; diff --git a/src/studio-home/processing-courses/messages.js b/src/studio-home/processing-courses/messages.js new file mode 100644 index 0000000000..98d90ccf36 --- /dev/null +++ b/src/studio-home/processing-courses/messages.js @@ -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; diff --git a/src/studio-home/scss/StudioHome.scss b/src/studio-home/scss/StudioHome.scss new file mode 100644 index 0000000000..0f1d2c3fa8 --- /dev/null +++ b/src/studio-home/scss/StudioHome.scss @@ -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; +} diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx new file mode 100644 index 0000000000..d8abe0e1a5 --- /dev/null +++ b/src/studio-home/tabs-section/TabsSection.test.jsx @@ -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 = () => ( + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + useSelector.mockReturnValue(studioHomeMock); + }); + it('should render all tabs correctly', () => { + const { getByText } = render(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + const librariesTab = getByText(messages.librariesTabTitle.defaultMessage); + fireEvent.click(librariesTab); + waitFor(() => { + expect(window.location.href).toBe(studioHomeMock.libraryAuthoringMfeUrl); + }); + }); +}); diff --git a/src/studio-home/tabs-section/archived-tab/index.jsx b/src/studio-home/tabs-section/archived-tab/index.jsx new file mode 100644 index 0000000000..3611d3a102 --- /dev/null +++ b/src/studio-home/tabs-section/archived-tab/index.jsx @@ -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 }) => ( +
+ {sortAlphabeticallyArray(archivedCoursesData).map(({ + courseKey, displayName, lmsLink, org, rerunLink, number, run, url, + }) => ( + + ))} +
+); + +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; diff --git a/src/studio-home/tabs-section/courses-tab/contact-administrator/index.jsx b/src/studio-home/tabs-section/courses-tab/contact-administrator/index.jsx new file mode 100644 index 0000000000..bbf2d80f3e --- /dev/null +++ b/src/studio-home/tabs-section/courses-tab/contact-administrator/index.jsx @@ -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 ( + + + {intl.formatMessage(messages.defaultSection_1_Description)} + + {hasAbilityToCreateCourse && ( + <> + + + {intl.formatMessage(messages.btnAddNewCourseText)} + + )} + > + {intl.formatMessage(messages.defaultSection_2_Description)} + + + )} + + ); +}; + +ContactAdministrator.defaultProps = { + hasAbilityToCreateCourse: false, +}; + +ContactAdministrator.propTypes = { + intl: intlShape.isRequired, + hasAbilityToCreateCourse: PropTypes.bool, + showNewCourseContainer: PropTypes.bool.isRequired, + onClickNewCourse: PropTypes.func.isRequired, +}; + +export default injectIntl(ContactAdministrator); diff --git a/src/studio-home/tabs-section/courses-tab/index.jsx b/src/studio-home/tabs-section/courses-tab/index.jsx new file mode 100644 index 0000000000..50990501d0 --- /dev/null +++ b/src/studio-home/tabs-section/courses-tab/index.jsx @@ -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, + }) => ( + + ), + ) + ) : (!optimizationEnabled && ( + + ) + )} + {showCollapsible && ( + + )} + + ); +}; + +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; diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.jsx new file mode 100644 index 0000000000..ea18f82af6 --- /dev/null +++ b/src/studio-home/tabs-section/index.jsx @@ -0,0 +1,128 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Tab, Tabs } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import { getStudioHomeData } from '../data/selectors'; +import messages from '../messages'; +import LibrariesTab from './libraries-tab'; +import ArchivedTab from './archived-tab'; +import CoursesTab from './courses-tab'; + +const TabsSection = ({ + intl, tabsData, showNewCourseContainer, onClickNewCourse, +}) => { + const TABS_LIST = { + courses: 'courses', + libraries: 'libraries', + archived: 'archived', + }; + const { + libraryAuthoringMfeUrl, + redirectToLibraryAuthoringMfe, + } = useSelector(getStudioHomeData); + const { + activeTab, courses, librariesEnabled, libraries, archivedCourses, + } = tabsData; + + // Controlling the visibility of tabs when using conditional rendering is necessary for + // the correct operation of iterating over child elements inside the Paragon Tabs component. + const visibleTabs = useMemo(() => { + const tabs = []; + tabs.push( + + + , + ); + + if (archivedCourses?.length) { + tabs.push( + + + , + ); + } + + if (librariesEnabled) { + tabs.push( + + {!redirectToLibraryAuthoringMfe && } + , + ); + } + + return tabs; + }, [archivedCourses, librariesEnabled, showNewCourseContainer]); + + const handleSelectTab = (tab) => { + if (tab === TABS_LIST.libraries && redirectToLibraryAuthoringMfe) { + window.location.href = libraryAuthoringMfeUrl; + } + }; + + return ( + + {visibleTabs} + + ); +}; + +const courseDataStructure = { + 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, +}; + +TabsSection.propTypes = { + intl: intlShape.isRequired, + tabsData: PropTypes.shape({ + activeTab: PropTypes.string.isRequired, + archivedCourses: PropTypes.arrayOf( + PropTypes.shape(courseDataStructure), + ).isRequired, + courses: PropTypes.arrayOf( + PropTypes.shape(courseDataStructure), + ).isRequired, + libraries: PropTypes.arrayOf( + PropTypes.shape({ + displayName: PropTypes.string.isRequired, + libraryKey: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + org: PropTypes.string.isRequired, + number: PropTypes.string.isRequired, + }), + ).isRequired, + librariesEnabled: PropTypes.bool.isRequired, + }).isRequired, + showNewCourseContainer: PropTypes.bool.isRequired, + onClickNewCourse: PropTypes.func.isRequired, +}; + +export default injectIntl(TabsSection); diff --git a/src/studio-home/tabs-section/libraries-tab/index.jsx b/src/studio-home/tabs-section/libraries-tab/index.jsx new file mode 100644 index 0000000000..e2f17c6fe6 --- /dev/null +++ b/src/studio-home/tabs-section/libraries-tab/index.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import CardItem from '../../card-item'; +import { sortAlphabeticallyArray } from '../utils'; + +const LibrariesTab = ({ libraries }) => ( +
+ {sortAlphabeticallyArray(libraries).map(({ + displayName, org, number, url, + }) => ( + + ))} +
+); + +LibrariesTab.propTypes = { + libraries: PropTypes.arrayOf( + PropTypes.shape({ + displayName: PropTypes.string.isRequired, + libraryKey: PropTypes.string.isRequired, + number: PropTypes.string.isRequired, + org: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + }), + ).isRequired, +}; + +export default LibrariesTab; diff --git a/src/studio-home/tabs-section/utils.js b/src/studio-home/tabs-section/utils.js new file mode 100644 index 0000000000..5d3822b8ed --- /dev/null +++ b/src/studio-home/tabs-section/utils.js @@ -0,0 +1,12 @@ +/** + * Alphabetical sorting for arrays of courses and libraries. + * + * @param {array} arr - Array of courses or libraries. + * @returns {array} - An array of alphabetically sorted courses or libraries. + */ +const sortAlphabeticallyArray = (arr) => [...arr] + .sort((firstArrayData, secondArrayData) => firstArrayData + .displayName.localeCompare(secondArrayData.displayName)); + +// eslint-disable-next-line import/prefer-default-export +export { sortAlphabeticallyArray }; diff --git a/src/studio-home/tabs-section/utils.test.js b/src/studio-home/tabs-section/utils.test.js new file mode 100644 index 0000000000..d10d56a07b --- /dev/null +++ b/src/studio-home/tabs-section/utils.test.js @@ -0,0 +1,26 @@ +import { sortAlphabeticallyArray } from './utils'; + +const testData = [ + { displayName: 'Apple' }, + { displayName: 'Orange' }, + { displayName: 'Banana' }, +]; + +describe('sortAlphabeticallyArray', () => { + it('sortAlphabeticallyArray sorts array alphabetically', () => { + const sortedData = sortAlphabeticallyArray(testData); + + expect(sortedData).toEqual([ + { displayName: 'Apple' }, + { displayName: 'Banana' }, + { displayName: 'Orange' }, + ]); + }); + + it('sortAlphabeticallyArray does not mutate the original array', () => { + const originalData = [...testData]; + sortAlphabeticallyArray(testData); + + expect(testData).toEqual(originalData); + }); +}); diff --git a/src/studio-home/verify-email-layout/VerifyEmailLayout.test.jsx b/src/studio-home/verify-email-layout/VerifyEmailLayout.test.jsx new file mode 100644 index 0000000000..ed5f507831 --- /dev/null +++ b/src/studio-home/verify-email-layout/VerifyEmailLayout.test.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; +import VerifyEmailLayout from '.'; + +const mockPathname = '/foo-bar'; +const fakeAuthenticatedUser = { + email: 'email@fake.com', + username: 'fake-user', +}; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: mockPathname, + }), +})); +jest.mock('@edx/frontend-platform/auth'); +getAuthenticatedUser.mockImplementation(() => fakeAuthenticatedUser); + +const RootWrapper = () => ( + + + +); + +describe('', () => { + it('renders successfully', () => { + const { getByText } = render(); + expect( + getByText(`Thanks for signing up, ${fakeAuthenticatedUser.username}!`, { + exact: false, + }), + ).toBeInTheDocument(); + expect(getByText(messages.bannerTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText( + `Almost there! In order to complete your sign up we need you to verify your email address (${fakeAuthenticatedUser.email}). An activation message and next steps should be waiting for you there.`, + { exact: false }, + )).toBeInTheDocument(); + expect(getByText(messages.sidebarTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.sidebarDescription.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/studio-home/verify-email-layout/index.jsx b/src/studio-home/verify-email-layout/index.jsx new file mode 100644 index 0000000000..4a394e0333 --- /dev/null +++ b/src/studio-home/verify-email-layout/index.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Layout, Card } from '@edx/paragon'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { HelpSidebar } from '../../generic/help-sidebar'; +import messages from './messages'; + +const VerifyEmailLayout = () => { + const intl = useIntl(); + const { email, username } = getAuthenticatedUser(); + + return ( + + +
+

{intl.formatMessage(messages.headingTitle, { username })}

+ + + {intl.formatMessage(messages.bannerDescription, { email })} + + +
+
+ + +

+ {intl.formatMessage(messages.sidebarTitle)} +

+

+ {intl.formatMessage(messages.sidebarDescription)} +

+
+
+
+ ); +}; + +export default VerifyEmailLayout; diff --git a/src/studio-home/verify-email-layout/messages.js b/src/studio-home/verify-email-layout/messages.js new file mode 100644 index 0000000000..a7cd32b787 --- /dev/null +++ b/src/studio-home/verify-email-layout/messages.js @@ -0,0 +1,26 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + headingTitle: { + id: 'course-authoring.studio-home.verify-email.heading', + defaultMessage: 'Thanks for signing up, {username}!', + }, + bannerTitle: { + id: 'course-authoring.studio-home.verify-email.banner.title', + defaultMessage: 'We need to verify your email address', + }, + bannerDescription: { + id: 'course-authoring.studio-home.verify-email.banner.description', + defaultMessage: '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.', + }, + sidebarTitle: { + id: 'course-authoring.studio-home.verify-email.sidebar.title', + defaultMessage: 'Need help?', + }, + sidebarDescription: { + id: 'course-authoring.studio-home.verify-email.sidebar.description', + defaultMessage: '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.', + }, +}); + +export default messages;