From 188899311345c7fd792a6786593e6c9d2c68d0ac Mon Sep 17 00:00:00 2001 From: Kyrylo Kholodenko Date: Thu, 14 Sep 2023 16:07:24 +0300 Subject: [PATCH] feat: implement export page (#586) --- .env | 1 - .env.development | 1 - .env.test | 1 - package-lock.json | 1 + package.json | 1 + src/CourseAuthoringRoutes.jsx | 6 +- src/export-page/CourseExportPage.jsx | 127 ++++++++++++++++++ src/export-page/CourseExportPage.scss | 9 ++ src/export-page/CourseExportPage.test.jsx | 127 ++++++++++++++++++ src/export-page/__mocks__/exportPage.js | 3 + src/export-page/__mocks__/index.js | 2 + src/export-page/data/api.js | 19 +++ src/export-page/data/api.test.js | 47 +++++++ src/export-page/data/constants.js | 8 ++ src/export-page/data/selectors.js | 8 ++ src/export-page/data/slice.js | 63 +++++++++ src/export-page/data/thunks.js | 75 +++++++++++ .../export-footer/ExportFooter.jsx | 48 +++++++ .../export-footer/ExportFooter.scss | 14 ++ src/export-page/export-footer/messages.js | 58 ++++++++ .../export-modal-error/ExportModalError.jsx | 54 ++++++++ .../export-modal-error/messages.js | 34 +++++ .../export-sidebar/ExportSidebar.jsx | 49 +++++++ .../export-sidebar/ExportSidebar.scss | 4 + .../export-sidebar/ExportSidebar.test.jsx | 39 ++++++ src/export-page/export-sidebar/messages.js | 66 +++++++++ .../export-stepper/ExportStepper.jsx | 89 ++++++++++++ .../export-stepper/ExportStepper.scss | 3 + .../export-stepper/ExportStepper.test.jsx | 38 ++++++ src/export-page/export-stepper/messages.js | 46 +++++++ src/export-page/messages.js | 34 +++++ src/export-page/utils.js | 29 ++++ src/export-page/utils.test.js | 51 +++++++ .../course-stepper/CourseStepper.test.jsx | 107 +++++++++++++++ src/generic/course-stepper/CouseStepper.scss | 68 ++++++++++ src/generic/course-stepper/index.jsx | 114 ++++++++++++++++ src/generic/modal-error/ModalError.jsx | 33 +++++ src/generic/styles.scss | 1 + src/i18n/messages/ar.json | 53 +++++++- src/i18n/messages/de.json | 53 +++++++- src/i18n/messages/de_DE.json | 53 +++++++- src/i18n/messages/es_419.json | 53 +++++++- src/i18n/messages/fr.json | 53 +++++++- src/i18n/messages/fr_CA.json | 53 +++++++- src/i18n/messages/hi.json | 53 +++++++- src/i18n/messages/it.json | 53 +++++++- src/i18n/messages/it_IT.json | 53 +++++++- src/i18n/messages/pt.json | 53 +++++++- src/i18n/messages/pt_PT.json | 53 +++++++- src/i18n/messages/ru.json | 53 +++++++- src/i18n/messages/uk.json | 53 +++++++- src/i18n/messages/zh_CN.json | 53 +++++++- src/index.scss | 1 + src/store.js | 2 + 54 files changed, 2202 insertions(+), 21 deletions(-) create mode 100644 src/export-page/CourseExportPage.jsx create mode 100644 src/export-page/CourseExportPage.scss create mode 100644 src/export-page/CourseExportPage.test.jsx create mode 100644 src/export-page/__mocks__/exportPage.js create mode 100644 src/export-page/__mocks__/index.js create mode 100644 src/export-page/data/api.js create mode 100644 src/export-page/data/api.test.js create mode 100644 src/export-page/data/constants.js create mode 100644 src/export-page/data/selectors.js create mode 100644 src/export-page/data/slice.js create mode 100644 src/export-page/data/thunks.js create mode 100644 src/export-page/export-footer/ExportFooter.jsx create mode 100644 src/export-page/export-footer/ExportFooter.scss create mode 100644 src/export-page/export-footer/messages.js create mode 100644 src/export-page/export-modal-error/ExportModalError.jsx create mode 100644 src/export-page/export-modal-error/messages.js create mode 100644 src/export-page/export-sidebar/ExportSidebar.jsx create mode 100644 src/export-page/export-sidebar/ExportSidebar.scss create mode 100644 src/export-page/export-sidebar/ExportSidebar.test.jsx create mode 100644 src/export-page/export-sidebar/messages.js create mode 100644 src/export-page/export-stepper/ExportStepper.jsx create mode 100644 src/export-page/export-stepper/ExportStepper.scss create mode 100644 src/export-page/export-stepper/ExportStepper.test.jsx create mode 100644 src/export-page/export-stepper/messages.js create mode 100644 src/export-page/messages.js create mode 100644 src/export-page/utils.js create mode 100644 src/export-page/utils.test.js create mode 100644 src/generic/course-stepper/CourseStepper.test.jsx create mode 100644 src/generic/course-stepper/CouseStepper.scss create mode 100644 src/generic/course-stepper/index.jsx create mode 100644 src/generic/modal-error/ModalError.jsx diff --git a/.env b/.env index 452df8210f..3df6415dca 100644 --- a/.env +++ b/.env @@ -36,7 +36,6 @@ ENABLE_NEW_VIDEO_UPLOAD_PAGE = false ENABLE_NEW_GRADING_PAGE = false ENABLE_NEW_COURSE_TEAM_PAGE = false ENABLE_NEW_IMPORT_PAGE = false -ENABLE_NEW_EXPORT_PAGE = false ENABLE_UNIT_PAGE = false ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false BBB_LEARN_MORE_URL='' diff --git a/.env.development b/.env.development index 184972f459..b3ccd5f655 100644 --- a/.env.development +++ b/.env.development @@ -38,7 +38,6 @@ ENABLE_NEW_VIDEO_UPLOAD_PAGE = false ENABLE_NEW_GRADING_PAGE = false ENABLE_NEW_COURSE_TEAM_PAGE = false ENABLE_NEW_IMPORT_PAGE = false -ENABLE_NEW_EXPORT_PAGE = false ENABLE_UNIT_PAGE = false ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false BBB_LEARN_MORE_URL='' diff --git a/.env.test b/.env.test index f97e6c8932..8118258480 100644 --- a/.env.test +++ b/.env.test @@ -34,7 +34,6 @@ ENABLE_NEW_VIDEO_UPLOAD_PAGE = true ENABLE_NEW_GRADING_PAGE = true ENABLE_NEW_COURSE_TEAM_PAGE = true ENABLE_NEW_IMPORT_PAGE = true -ENABLE_NEW_EXPORT_PAGE = true ENABLE_UNIT_PAGE = true ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = true BBB_LEARN_MORE_URL='' diff --git a/package-lock.json b/package-lock.json index 78b04c33eb..4323cb5960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "react-transition-group": "4.4.1", "redux": "4.0.5", "regenerator-runtime": "0.13.7", + "universal-cookie": "^4.0.4", "uuid": "^3.4.0", "yup": "0.31.1" }, diff --git a/package.json b/package.json index 5b046f600e..bcfb21b80f 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-transition-group": "4.4.1", "redux": "4.0.5", "regenerator-runtime": "0.13.7", + "universal-cookie": "^4.0.4", "uuid": "^3.4.0", "yup": "0.31.1" }, diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index 1d886b0f86..3af7b0e74d 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -15,6 +15,7 @@ import ScheduleAndDetails from './schedule-and-details'; import { GradingSettings } from './grading-settings'; import CourseTeam from './course-team/CourseTeam'; import { CourseUpdates } from './course-updates'; +import CourseExportPage from './export-page/CourseExportPage'; /** * As of this writing, these routes are mounted at a path prefixed with the following: @@ -105,10 +106,7 @@ const CourseAuthoringRoutes = ({ courseId }) => { )} - {process.env.ENABLE_NEW_EXPORT_PAGE === 'true' - && ( - - )} + diff --git a/src/export-page/CourseExportPage.jsx b/src/export-page/CourseExportPage.jsx new file mode 100644 index 0000000000..9d3d9e698e --- /dev/null +++ b/src/export-page/CourseExportPage.jsx @@ -0,0 +1,127 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { + Container, Layout, Button, Card, +} from '@edx/paragon'; +import { ArrowCircleDown as ArrowCircleDownIcon } from '@edx/paragon/icons'; +import Cookies from 'universal-cookie'; +import { getConfig } from '@edx/frontend-platform'; +import { Helmet } from 'react-helmet'; + +import InternetConnectionAlert from '../generic/internet-connection-alert'; +import SubHeader from '../generic/sub-header/SubHeader'; +import { RequestStatus } from '../data/constants'; +import { useModel } from '../generic/model-store'; +import messages from './messages'; +import ExportSidebar from './export-sidebar/ExportSidebar'; +import { + getCurrentStage, getError, getExportTriggered, getLoadingStatus, getSavingStatus, +} from './data/selectors'; +import { startExportingCourse } from './data/thunks'; +import { EXPORT_STAGES, LAST_EXPORT_COOKIE_NAME } from './data/constants'; +import { updateExportTriggered, updateSavingStatus, updateSuccessDate } from './data/slice'; +import ExportModalError from './export-modal-error/ExportModalError'; +import ExportFooter from './export-footer/ExportFooter'; +import ExportStepper from './export-stepper/ExportStepper'; + +const CourseExportPage = ({ intl, courseId }) => { + const dispatch = useDispatch(); + const exportTriggered = useSelector(getExportTriggered); + const courseDetails = useModel('courseDetails', courseId); + const currentStage = useSelector(getCurrentStage); + const { msg: errorMessage } = useSelector(getError); + const loadingStatus = useSelector(getLoadingStatus); + const savingStatus = useSelector(getSavingStatus); + const cookies = new Cookies(); + const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS; + const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED; + const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS; + + useEffect(() => { + const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME); + if (cookieData) { + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + dispatch(updateExportTriggered(true)); + dispatch(updateSuccessDate(cookieData.date)); + } + }, []); + + return ( + <> + + + {intl.formatMessage(messages.pageTitle, { + headingTitle: intl.formatMessage(messages.headingTitle), + courseName: courseDetails?.name, + siteName: process.env.SITE_NAME, + })} + + + +
+ + +
+ +

{intl.formatMessage(messages.description1, { studioShortName: getConfig().STUDIO_SHORT_NAME })}

+

{intl.formatMessage(messages.description2)}

+ + + {isShowExportButton && ( + + + + )} + + {exportTriggered && } + +
+
+ + + +
+
+ +
+
+ null} + /> +
+ + ); +}; + +CourseExportPage.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +CourseExportPage.defaultProps = {}; + +export default injectIntl(CourseExportPage); diff --git a/src/export-page/CourseExportPage.scss b/src/export-page/CourseExportPage.scss new file mode 100644 index 0000000000..a4db894a28 --- /dev/null +++ b/src/export-page/CourseExportPage.scss @@ -0,0 +1,9 @@ +@import "./export-stepper/ExportStepper"; +@import "./export-footer/ExportFooter"; +@import "./export-sidebar/ExportSidebar"; + +.export { + .help-sidebar { + margin-top: 7.188rem; + } +} diff --git a/src/export-page/CourseExportPage.test.jsx b/src/export-page/CourseExportPage.test.jsx new file mode 100644 index 0000000000..8c8524febd --- /dev/null +++ b/src/export-page/CourseExportPage.test.jsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { getConfig, 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 { fireEvent, render, waitFor } from '@testing-library/react'; +import MockAdapter from 'axios-mock-adapter'; +import { Helmet } from 'react-helmet'; + +import Cookies from 'universal-cookie'; +import initializeStore from '../store'; +import stepperMessages from './export-stepper/messages'; +import modalErrorMessages from './export-modal-error/messages'; +import { getExportStatusApiUrl, postExportCourseApiUrl } from './data/api'; +import { EXPORT_STAGES } from './data/constants'; +import { exportPageMock } from './__mocks__'; +import messages from './messages'; +import CourseExportPage from './CourseExportPage'; + +let store; +let axiosMock; +let cookies; +const courseId = '123'; +const courseName = 'About Node JS'; + +jest.mock('../generic/model-store', () => ({ + useModel: jest.fn().mockReturnValue({ + name: courseName, + }), +})); + +jest.mock('universal-cookie', () => { + const mCookie = { + get: jest.fn(), + set: jest.fn(), + }; + return jest.fn(() => mCookie); +}); + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(postExportCourseApiUrl(courseId)) + .reply(200, exportPageMock); + cookies = new Cookies(); + cookies.get.mockReturnValue(null); + }); + it('should render page title correctly', async () => { + render(); + await waitFor(() => { + const helmet = Helmet.peek(); + expect(helmet.title).toEqual( + `${messages.headingTitle.defaultMessage} | ${courseName} | ${process.env.SITE_NAME}`, + ); + }); + }); + it('should render without errors', async () => { + const { getByText } = render(); + await waitFor(() => { + expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); + const exportPageElement = getByText(messages.headingTitle.defaultMessage, { + selector: 'h2.sub-header-title', + }); + expect(exportPageElement).toBeInTheDocument(); + expect(getByText(messages.titleUnderButton.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument(); + }); + }); + it('should start exporting on click', async () => { + const { getByText, container } = render(); + const button = container.querySelector('.btn-primary'); + fireEvent.click(button); + expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument(); + }); + it('should show modal error', async () => { + axiosMock + .onGet(getExportStatusApiUrl(courseId)) + .reply(200, { exportStatus: EXPORT_STAGES.EXPORTING, exportError: { rawErrorMsg: 'test error', editUnitUrl: 'http://test-url.test' } }); + const { getByText, queryByText, container } = render(); + const startExportButton = container.querySelector('.btn-primary'); + fireEvent.click(startExportButton); + // eslint-disable-next-line no-promise-executor-return + await new Promise((r) => setTimeout(r, 3500)); + expect(getByText(/There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: test error/i)); + const closeModalWindowButton = getByText('Return to export'); + fireEvent.click(closeModalWindowButton); + expect(queryByText(modalErrorMessages.errorCancelButtonUnit.defaultMessage)).not.toBeInTheDocument(); + fireEvent.click(closeModalWindowButton); + }); + it('should fetch status without clicking when cookies has', async () => { + cookies.get.mockReturnValue({ date: 1679787000 }); + const { getByText } = render(); + expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument(); + }); + it('should show download path', async () => { + axiosMock + .onGet(getExportStatusApiUrl(courseId)) + .reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: '/test-download-path.test' }); + const { getByText, container } = render(); + const startExportButton = container.querySelector('.btn-primary'); + fireEvent.click(startExportButton); + // eslint-disable-next-line no-promise-executor-return + await new Promise((r) => setTimeout(r, 3500)); + const downloadButton = getByText(stepperMessages.downloadCourseButtonTitle.defaultMessage); + expect(downloadButton).toBeInTheDocument(); + expect(downloadButton.getAttribute('href')).toEqual(`${getConfig().STUDIO_BASE_URL}/test-download-path.test`); + }); +}); diff --git a/src/export-page/__mocks__/exportPage.js b/src/export-page/__mocks__/exportPage.js new file mode 100644 index 0000000000..5380633d9c --- /dev/null +++ b/src/export-page/__mocks__/exportPage.js @@ -0,0 +1,3 @@ +module.exports = { + exportStatus: 1, +}; diff --git a/src/export-page/__mocks__/index.js b/src/export-page/__mocks__/index.js new file mode 100644 index 0000000000..ca6970871f --- /dev/null +++ b/src/export-page/__mocks__/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as exportPageMock } from './exportPage'; diff --git a/src/export-page/data/api.js b/src/export-page/data/api.js new file mode 100644 index 0000000000..68d61c10ee --- /dev/null +++ b/src/export-page/data/api.js @@ -0,0 +1,19 @@ +/* eslint-disable import/prefer-default-export */ +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; +export const postExportCourseApiUrl = (courseId) => new URL(`export/${courseId}`, getApiBaseUrl()).href; +export const getExportStatusApiUrl = (courseId) => new URL(`export_status/${courseId}`, getApiBaseUrl()).href; + +export async function startCourseExporting(courseId) { + const { data } = await getAuthenticatedHttpClient() + .post(postExportCourseApiUrl(courseId)); + return camelCaseObject(data); +} + +export async function getExportStatus(courseId) { + const { data } = await getAuthenticatedHttpClient() + .get(getExportStatusApiUrl(courseId)); + return camelCaseObject(data); +} diff --git a/src/export-page/data/api.test.js b/src/export-page/data/api.test.js new file mode 100644 index 0000000000..70acc8b243 --- /dev/null +++ b/src/export-page/data/api.test.js @@ -0,0 +1,47 @@ +import MockAdapter from 'axios-mock-adapter'; +import { initializeMockApp, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { getExportStatus, postExportCourseApiUrl, startCourseExporting } from './api'; + +let axiosMock; +const courseId = 'course-123'; + +describe('API Functions', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch status on start exporting', async () => { + const data = { exportStatus: 1 }; + axiosMock.onPost(postExportCourseApiUrl(courseId)).reply(200, data); + + const result = await startCourseExporting(courseId); + + expect(axiosMock.history.post[0].url).toEqual(postExportCourseApiUrl(courseId)); + expect(result).toEqual(data); + }); + + it('should fetch on get export status', async () => { + const data = { exportStatus: 2 }; + const queryUrl = new URL(`export_status/${courseId}`, getConfig().STUDIO_BASE_URL).href; + axiosMock.onGet(queryUrl).reply(200, data); + + const result = await getExportStatus(courseId); + + expect(axiosMock.history.get[0].url).toEqual(queryUrl); + expect(result).toEqual(data); + }); +}); diff --git a/src/export-page/data/constants.js b/src/export-page/data/constants.js new file mode 100644 index 0000000000..5824c61c7d --- /dev/null +++ b/src/export-page/data/constants.js @@ -0,0 +1,8 @@ +export const LAST_EXPORT_COOKIE_NAME = 'lastexport'; +export const EXPORT_STAGES = { + PREPARING: 0, + EXPORTING: 1, + COMPRESSING: 2, + SUCCESS: 3, +}; +export const SUCCESS_DATE_FORMAT = 'MM/DD/yyyy'; diff --git a/src/export-page/data/selectors.js b/src/export-page/data/selectors.js new file mode 100644 index 0000000000..2c6d0d737f --- /dev/null +++ b/src/export-page/data/selectors.js @@ -0,0 +1,8 @@ +export const getExportTriggered = (state) => state.courseExport.exportTriggered; +export const getCurrentStage = (state) => state.courseExport.currentStage; +export const getDownloadPath = (state) => state.courseExport.downloadPath; +export const getSuccessDate = (state) => state.courseExport.successDate; +export const getError = (state) => state.courseExport.error; +export const getIsErrorModalOpen = (state) => state.courseExport.isErrorModalOpen; +export const getLoadingStatus = (state) => state.courseExport.loadingStatus; +export const getSavingStatus = (state) => state.courseExport.savingStatus; diff --git a/src/export-page/data/slice.js b/src/export-page/data/slice.js new file mode 100644 index 0000000000..f431a69369 --- /dev/null +++ b/src/export-page/data/slice.js @@ -0,0 +1,63 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + exportTriggered: false, + currentStage: 0, + error: { msg: null, unitUrl: null }, + downloadPath: null, + successDate: null, + isErrorModalOpen: false, + loadingStatus: '', + savingStatus: '', +}; + +const slice = createSlice({ + name: 'exportPage', + initialState, + reducers: { + updateExportTriggered: (state, { payload }) => { + state.exportTriggered = payload; + }, + updateCurrentStage: (state, { payload }) => { + if (payload >= state.currentStage) { + state.currentStage = payload; + } + }, + updateDownloadPath: (state, { payload }) => { + state.downloadPath = payload; + }, + updateSuccessDate: (state, { payload }) => { + state.successDate = payload; + }, + updateError: (state, { payload }) => { + state.error = payload; + }, + updateIsErrorModalOpen: (state, { payload }) => { + state.isErrorModalOpen = payload; + }, + reset: () => initialState, + updateLoadingStatus: (state, { payload }) => { + state.loadingStatus = payload.status; + }, + updateSavingStatus: (state, { payload }) => { + state.savingStatus = payload.status; + }, + }, +}); + +export const { + updateExportTriggered, + updateCurrentStage, + updateDownloadPath, + updateSuccessDate, + updateError, + updateIsErrorModalOpen, + reset, + updateLoadingStatus, + updateSavingStatus, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/export-page/data/thunks.js b/src/export-page/data/thunks.js new file mode 100644 index 0000000000..ceded5cb8d --- /dev/null +++ b/src/export-page/data/thunks.js @@ -0,0 +1,75 @@ +import Cookies from 'universal-cookie'; +import moment from 'moment'; + +import { RequestStatus } from '../../data/constants'; +import { setExportCookie } from '../utils'; +import { EXPORT_STAGES, LAST_EXPORT_COOKIE_NAME } from './constants'; + +import { + startCourseExporting, + getExportStatus, +} from './api'; +import { + updateExportTriggered, + updateCurrentStage, + updateDownloadPath, + updateSuccessDate, + updateError, + updateIsErrorModalOpen, + reset, + updateLoadingStatus, + updateSavingStatus, +} from './slice'; + +export function startExportingCourse(courseId) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + try { + dispatch(reset()); + dispatch(updateExportTriggered(true)); + const exportData = await startCourseExporting(courseId); + dispatch(updateCurrentStage(exportData.exportStatus)); + setExportCookie(moment().valueOf(), exportData.exportStatus === EXPORT_STAGES.SUCCESS); + + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + return true; + } catch (error) { + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + return false; + } + }; +} + +export function fetchExportStatus(courseId) { + return async (dispatch) => { + dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS })); + try { + const { exportStatus, exportOutput, exportError } = await getExportStatus(courseId); + dispatch(updateCurrentStage(Math.abs(exportStatus))); + + if (exportOutput) { + dispatch(updateDownloadPath(exportOutput)); + dispatch(updateSuccessDate(moment().valueOf())); + } + + const cookies = new Cookies(); + const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME); + if (!cookieData?.completed) { + setExportCookie(moment().valueOf(), exportStatus === EXPORT_STAGES.SUCCESS); + } + + if (exportError) { + const errorMessage = exportError.rawErrorMsg || exportError; + const errorUnitUrl = exportError.editUnitUrl || null; + dispatch(updateError({ msg: errorMessage, unitUrl: errorUnitUrl })); + dispatch(updateIsErrorModalOpen(true)); + } + + dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL })); + return true; + } catch (error) { + dispatch(updateLoadingStatus({ status: RequestStatus.FAILED })); + return false; + } + }; +} diff --git a/src/export-page/export-footer/ExportFooter.jsx b/src/export-page/export-footer/ExportFooter.jsx new file mode 100644 index 0000000000..58b5d0e5b0 --- /dev/null +++ b/src/export-page/export-footer/ExportFooter.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; +import { Layout } from '@edx/paragon'; + +import messages from './messages'; + +const ExportFooter = ({ intl }) => ( +
+ + +

{intl.formatMessage(messages.exportedDataTitle)}

+
    +
  • {intl.formatMessage(messages.exportedDataItem1)}
  • +
  • {intl.formatMessage(messages.exportedDataItem2)}
  • +
  • {intl.formatMessage(messages.exportedDataItem3)}
  • +
  • {intl.formatMessage(messages.exportedDataItem4)}
  • +
  • {intl.formatMessage(messages.exportedDataItem5)}
  • +
  • {intl.formatMessage(messages.exportedDataItem6)}
  • +
  • {intl.formatMessage(messages.exportedDataItem7)}
  • +
+
+ +

{intl.formatMessage(messages.notExportedDataTitle)}

+
    +
  • {intl.formatMessage(messages.notExportedDataItem1)}
  • +
  • {intl.formatMessage(messages.notExportedDataItem2)}
  • +
  • {intl.formatMessage(messages.notExportedDataItem3)}
  • +
  • {intl.formatMessage(messages.notExportedDataItem4)}
  • +
+
+
+
+); + +ExportFooter.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(ExportFooter); diff --git a/src/export-page/export-footer/ExportFooter.scss b/src/export-page/export-footer/ExportFooter.scss new file mode 100644 index 0000000000..33c04ac650 --- /dev/null +++ b/src/export-page/export-footer/ExportFooter.scss @@ -0,0 +1,14 @@ +.export-footer-list { + list-style: none; + padding-left: 0; + + li { + padding-bottom: .3125rem; + border-bottom: 1px solid #E5E5E5; + margin-bottom: .3125rem; + } + + li:last-child { + border-bottom: none; + } +} diff --git a/src/export-page/export-footer/messages.js b/src/export-page/export-footer/messages.js new file mode 100644 index 0000000000..a11d5b7ade --- /dev/null +++ b/src/export-page/export-footer/messages.js @@ -0,0 +1,58 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + exportedDataTitle: { + id: 'course-authoring.export.footer.exportedData.title', + defaultMessage: 'Data exported with your course:', + }, + exportedDataItem1: { + id: 'course-authoring.export.footer.exportedData.item.1', + defaultMessage: 'Values from Advanced settings, including MATLAB API keys and LTI passports', + }, + exportedDataItem2: { + id: 'course-authoring.export.footer.exportedData.item.2', + defaultMessage: 'Course content (all sections, sub-sections, and units)', + }, + exportedDataItem3: { + id: 'course-authoring.export.footer.exportedData.item.3', + defaultMessage: 'Course structure', + }, + exportedDataItem4: { + id: 'course-authoring.export.footer.exportedData.item.4', + defaultMessage: 'Individual problems', + }, + exportedDataItem5: { + id: 'course-authoring.export.footer.exportedData.item.5', + defaultMessage: 'Pages', + }, + exportedDataItem6: { + id: 'course-authoring.export.footer.exportedData.item.6', + defaultMessage: 'Course assets', + }, + exportedDataItem7: { + id: 'course-authoring.export.footer.exportedData.item.7', + defaultMessage: 'Course settings', + }, + notExportedDataTitle: { + id: 'course-authoring.export.footer.notExportedData.title', + defaultMessage: 'Data not exported with your course:', + }, + notExportedDataItem1: { + id: 'course-authoring.export.footer.notExportedData.item.1', + defaultMessage: 'User data', + }, + notExportedDataItem2: { + id: 'course-authoring.export.footer.notExportedData.item.2', + defaultMessage: 'Course team data', + }, + notExportedDataItem3: { + id: 'course-authoring.export.footer.notExportedData.item.3', + defaultMessage: 'Forum/discussion data', + }, + notExportedDataItem4: { + id: 'course-authoring.export.footer.notExportedData.item.4', + defaultMessage: 'Certificates', + }, +}); + +export default messages; diff --git a/src/export-page/export-modal-error/ExportModalError.jsx b/src/export-page/export-modal-error/ExportModalError.jsx new file mode 100644 index 0000000000..bff597db12 --- /dev/null +++ b/src/export-page/export-modal-error/ExportModalError.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useDispatch, useSelector } from 'react-redux'; +import { history } from '@edx/frontend-platform'; +import PropTypes from 'prop-types'; + +import ModalError from '../../generic/modal-error/ModalError'; +import { getError, getIsErrorModalOpen } from '../data/selectors'; +import { updateIsErrorModalOpen } from '../data/slice'; +import messages from './messages'; + +const ExportModalError = ({ + intl, + courseId, +}) => { + const dispatch = useDispatch(); + const isErrorModalOpen = useSelector(getIsErrorModalOpen); + const { msg: errorMessage, unitUrl: unitErrorUrl } = useSelector(getError); + + const handleUnitRedirect = () => { window.location.href = unitErrorUrl; }; + const handleRedirectCourseHome = () => history.push(`/course/${courseId}/outline`); + + return ( + dispatch(updateIsErrorModalOpen(false))} + handleAction={unitErrorUrl ? handleUnitRedirect : handleRedirectCourseHome} + /> + ); +}; + +ExportModalError.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +ExportModalError.defaultProps = {}; + +export default injectIntl(ExportModalError); diff --git a/src/export-page/export-modal-error/messages.js b/src/export-page/export-modal-error/messages.js new file mode 100644 index 0000000000..b691db3ebc --- /dev/null +++ b/src/export-page/export-modal-error/messages.js @@ -0,0 +1,34 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + errorTitle: { + id: 'course-authoring.export.modal.error.title', + defaultMessage: 'There has been an error while exporting.', + }, + errorDescriptionNotUnit: { + id: 'course-authoring.export.modal.error.description.not.unit', + defaultMessage: 'Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}', + }, + errorDescriptionUnit: { + id: 'course-authoring.export.modal.error.description.unit', + defaultMessage: 'There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}', + }, + errorCancelButtonUnit: { + id: 'course-authoring.export.modal.error.button.cancel.unit', + defaultMessage: 'Return to export', + }, + errorCancelButtonNotUnit: { + id: 'course-authoring.export.modal.error.button.cancel.not.unit', + defaultMessage: 'Cancel', + }, + errorActionButtonNotUnit: { + id: 'course-authoring.export.modal.error.button.action.not.unit', + defaultMessage: 'Take me to the main course page', + }, + errorActionButtonUnit: { + id: 'course-authoring.export.modal.error.button.action.unit', + defaultMessage: 'Correct failed component', + }, +}); + +export default messages; diff --git a/src/export-page/export-sidebar/ExportSidebar.jsx b/src/export-page/export-sidebar/ExportSidebar.jsx new file mode 100644 index 0000000000..0d5f950901 --- /dev/null +++ b/src/export-page/export-sidebar/ExportSidebar.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; +import PropTypes from 'prop-types'; +import { Button } from '@edx/paragon'; +import { getConfig } from '@edx/frontend-platform'; + +import HelpSidebar from '../../generic/help-sidebar'; +import { useHelpUrls } from '../../help-urls/hooks'; +import messages from './messages'; + +const ExportSidebar = ({ intl, courseId }) => { + const { exportCourse: exportLearnMoreUrl } = useHelpUrls(['exportCourse']); + return ( + +

{intl.formatMessage(messages.title1)}

+

{intl.formatMessage(messages.description1, { studioShortName: getConfig().STUDIO_SHORT_NAME })}

+
+

{intl.formatMessage(messages.exportedContent)}

+

{intl.formatMessage(messages.exportedContentHeading)}

+
    +
  • {intl.formatMessage(messages.content1)}
  • +
  • {intl.formatMessage(messages.content2)}
  • +
  • {intl.formatMessage(messages.content3)}
  • +
  • {intl.formatMessage(messages.content4)}
  • +
  • {intl.formatMessage(messages.content5)}
  • +
+

{intl.formatMessage(messages.notExportedContent)}

+
    +
  • {intl.formatMessage(messages.content6)}
  • +
  • {intl.formatMessage(messages.content7)}
  • +
+
+

{intl.formatMessage(messages.openDownloadFile)}

+

{intl.formatMessage(messages.openDownloadFileDescription)}

+
+ +
+ ); +}; + +ExportSidebar.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +export default injectIntl(ExportSidebar); diff --git a/src/export-page/export-sidebar/ExportSidebar.scss b/src/export-page/export-sidebar/ExportSidebar.scss new file mode 100644 index 0000000000..2247058d4f --- /dev/null +++ b/src/export-page/export-sidebar/ExportSidebar.scss @@ -0,0 +1,4 @@ +.export-sidebar-list { + list-style: none; + padding-left: 0; +} diff --git a/src/export-page/export-sidebar/ExportSidebar.test.jsx b/src/export-page/export-sidebar/ExportSidebar.test.jsx new file mode 100644 index 0000000000..d770f4b156 --- /dev/null +++ b/src/export-page/export-sidebar/ExportSidebar.test.jsx @@ -0,0 +1,39 @@ +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 messages from './messages'; +import ExportSidebar from './ExportSidebar'; + +const courseId = 'course-123'; +let store; + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + }); + it('render sidebar correctly', () => { + const { getByText } = render(); + expect(getByText(messages.title1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.exportedContentHeading.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/export-page/export-sidebar/messages.js b/src/export-page/export-sidebar/messages.js new file mode 100644 index 0000000000..0917cd866e --- /dev/null +++ b/src/export-page/export-sidebar/messages.js @@ -0,0 +1,66 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + title1: { + id: 'course-authoring.export.sidebar.title1', + defaultMessage: 'Why export a course?', + }, + description1: { + id: 'course-authoring.export.sidebar.description1', + defaultMessage: 'You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.', + }, + exportedContent: { + id: 'course-authoring.export.sidebar.exportedContent', + defaultMessage: 'What content is exported?', + }, + exportedContentHeading: { + id: 'course-authoring.export.sidebar.exportedContentHeading', + defaultMessage: 'The following content is exported.', + }, + content1: { + id: 'course-authoring.export.sidebar.content1', + defaultMessage: 'Course content and structure', + }, + content2: { + id: 'course-authoring.export.sidebar.content2', + defaultMessage: 'Course dates', + }, + content3: { + id: 'course-authoring.export.sidebar.content3', + defaultMessage: 'Grading policy', + }, + content4: { + id: 'course-authoring.export.sidebar.content4', + defaultMessage: 'Any group configurations', + }, + content5: { + id: 'course-authoring.export.sidebar.content5', + defaultMessage: 'Settings on the Advanced settings page, including MATLAB API keys and LTI passports', + }, + notExportedContent: { + id: 'course-authoring.export.sidebar.notExportedContent', + defaultMessage: 'The following content is not exported.', + }, + content6: { + id: 'course-authoring.export.sidebar.content6', + defaultMessage: 'Learner-specific content, such as learner grades and discussion forum data', + }, + content7: { + id: 'course-authoring.export.sidebar.content7', + defaultMessage: 'The course team', + }, + openDownloadFile: { + id: 'course-authoring.export.sidebar.openDownloadFile', + defaultMessage: 'Opening the downloaded file', + }, + openDownloadFileDescription: { + id: 'course-authoring.export.sidebar.openDownloadFileDescription', + defaultMessage: 'Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.', + }, + learnMoreButtonTitle: { + id: 'course-authoring.export.sidebar.learnMoreButtonTitle', + defaultMessage: 'Learn more about exporting a course', + }, +}); + +export default messages; diff --git a/src/export-page/export-stepper/ExportStepper.jsx b/src/export-page/export-stepper/ExportStepper.jsx new file mode 100644 index 0000000000..7a693d8b7e --- /dev/null +++ b/src/export-page/export-stepper/ExportStepper.jsx @@ -0,0 +1,89 @@ +import React, { useEffect } from 'react'; +import { + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { getConfig } from '@edx/frontend-platform'; +import { Button } from '@edx/paragon'; + +import CourseStepper from '../../generic/course-stepper'; +import { + getCurrentStage, getDownloadPath, getError, getLoadingStatus, getSuccessDate, +} from '../data/selectors'; +import { fetchExportStatus } from '../data/thunks'; +import { EXPORT_STAGES } from '../data/constants'; +import { getFormattedSuccessDate } from '../utils'; +import { RequestStatus } from '../../data/constants'; +import messages from './messages'; + +const ExportStepper = ({ intl, courseId }) => { + const currentStage = useSelector(getCurrentStage); + const downloadPath = useSelector(getDownloadPath); + const successDate = useSelector(getSuccessDate); + const loadingStatus = useSelector(getLoadingStatus); + const { msg: errorMessage } = useSelector(getError); + const dispatch = useDispatch(); + const isStopFetching = currentStage === EXPORT_STAGES.SUCCESS + || loadingStatus === RequestStatus.FAILED + || errorMessage; + + useEffect(() => { + const id = setInterval(() => { + if (isStopFetching) { + clearInterval(id); + } else { + dispatch(fetchExportStatus(courseId)); + } + }, 3000); + return () => clearInterval(id); + }); + + let successTitle = intl.formatMessage(messages.stepperSuccessTitle); + const formattedSuccessDate = getFormattedSuccessDate(successDate); + if (formattedSuccessDate && currentStage === EXPORT_STAGES.SUCCESS) { + successTitle += formattedSuccessDate; + } + const steps = [ + { + title: intl.formatMessage(messages.stepperPreparingTitle), + description: intl.formatMessage(messages.stepperPreparingDescription), + key: EXPORT_STAGES.PREPARING, + }, { + title: intl.formatMessage(messages.stepperExportingTitle), + description: intl.formatMessage(messages.stepperExportingDescription), + key: EXPORT_STAGES.EXPORTING, + }, { + title: intl.formatMessage(messages.stepperCompressingTitle), + description: intl.formatMessage(messages.stepperCompressingDescription), + key: EXPORT_STAGES.COMPRESSING, + }, { + title: successTitle, + description: intl.formatMessage(messages.stepperSuccessDescription), + key: EXPORT_STAGES.SUCCESS, + }, + ]; + + return ( +
+

{intl.formatMessage(messages.stepperHeaderTitle)}

+
+ + {downloadPath && currentStage === EXPORT_STAGES.SUCCESS && } +
+ ); +}; + +ExportStepper.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +export default injectIntl(ExportStepper); diff --git a/src/export-page/export-stepper/ExportStepper.scss b/src/export-page/export-stepper/ExportStepper.scss new file mode 100644 index 0000000000..ef2ab0f821 --- /dev/null +++ b/src/export-page/export-stepper/ExportStepper.scss @@ -0,0 +1,3 @@ +.pgn__stepper-header-step-list { + flex-direction: column; +} diff --git a/src/export-page/export-stepper/ExportStepper.test.jsx b/src/export-page/export-stepper/ExportStepper.test.jsx new file mode 100644 index 0000000000..c8ca1e4855 --- /dev/null +++ b/src/export-page/export-stepper/ExportStepper.test.jsx @@ -0,0 +1,38 @@ +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 messages from './messages'; +import ExportStepper from './ExportStepper'; + +const courseId = 'course-123'; +let store; + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + }); + it('render stepper correctly', () => { + const { getByText } = render(); + expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/export-page/export-stepper/messages.js b/src/export-page/export-stepper/messages.js new file mode 100644 index 0000000000..e0f4adfa1b --- /dev/null +++ b/src/export-page/export-stepper/messages.js @@ -0,0 +1,46 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + stepperPreparingTitle: { + id: 'course-authoring.export.stepper.title.preparing', + defaultMessage: 'Preparing', + }, + stepperExportingTitle: { + id: 'course-authoring.export.stepper.title.exporting', + defaultMessage: 'Exporting', + }, + stepperCompressingTitle: { + id: 'course-authoring.export.stepper.title.compressing', + defaultMessage: 'Compressing', + }, + stepperSuccessTitle: { + id: 'course-authoring.export.stepper.title.success', + defaultMessage: 'Success', + }, + stepperPreparingDescription: { + id: 'course-authoring.export.stepper.description.preparing', + defaultMessage: 'Preparing to start the export', + }, + stepperExportingDescription: { + id: 'course-authoring.export.stepper.description.exporting', + defaultMessage: 'Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)', + }, + stepperCompressingDescription: { + id: 'course-authoring.export.stepper.description.compressing', + defaultMessage: 'Compressing the exported data and preparing it for download', + }, + stepperSuccessDescription: { + id: 'course-authoring.export.stepper.description.success', + defaultMessage: 'Your exported course can now be downloaded', + }, + downloadCourseButtonTitle: { + id: 'course-authoring.export.stepper.download.button.title', + defaultMessage: 'Download exported course', + }, + stepperHeaderTitle: { + id: 'course-authoring.export.stepper.header.title', + defaultMessage: 'Course export status', + }, +}); + +export default messages; diff --git a/src/export-page/messages.js b/src/export-page/messages.js new file mode 100644 index 0000000000..2c8a44a8b2 --- /dev/null +++ b/src/export-page/messages.js @@ -0,0 +1,34 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + pageTitle: { + id: 'course-authoring.export.page.title', + defaultMessage: '{headingTitle} | {courseName} | {siteName}', + }, + headingTitle: { + id: 'course-authoring.export.heading.title', + defaultMessage: 'Course export', + }, + headingSubtitle: { + id: 'course-authoring.export.heading.subtitle', + defaultMessage: 'Tools', + }, + description1: { + id: 'course-authoring.export.description1', + defaultMessage: '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.', + }, + description2: { + id: 'course-authoring.export.description2', + defaultMessage: '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.', + }, + titleUnderButton: { + id: 'course-authoring.export.title-under-button', + defaultMessage: 'Export my course content', + }, + buttonTitle: { + id: 'course-authoring.export.button.title', + defaultMessage: 'Export course content', + }, +}); + +export default messages; diff --git a/src/export-page/utils.js b/src/export-page/utils.js new file mode 100644 index 0000000000..9f24b743b7 --- /dev/null +++ b/src/export-page/utils.js @@ -0,0 +1,29 @@ +import Cookies from 'universal-cookie'; +import moment from 'moment'; + +import { TIME_FORMAT } from '../constants'; +import { LAST_EXPORT_COOKIE_NAME, SUCCESS_DATE_FORMAT } from './data/constants'; + +/** + * Sets an export-related cookie with the provided information. + * + * @param {Date} date - Date of export. + * @param {boolean} completed - Indicates if export was completed successfully. + * @returns {void} + */ +export const setExportCookie = (date, completed) => { + const cookies = new Cookies(); + cookies.set(LAST_EXPORT_COOKIE_NAME, { date, completed }, { path: window.location.pathname }); +}; + +/** + * Formats a Unix timestamp as a formatted success date string. + * + * @param {number} unixDate - Unix timestamp to be formatted. + * @returns {string|null} Formatted success date string, including date and time in UTC, or null if the input is falsy. + */ +export const getFormattedSuccessDate = (unixDate) => { + const formattedDate = moment(unixDate).utc().format(SUCCESS_DATE_FORMAT); + const formattedTime = moment(unixDate).utc().format(TIME_FORMAT); + return unixDate ? ` (${formattedDate} at ${formattedTime} UTC)` : null; +}; diff --git a/src/export-page/utils.test.js b/src/export-page/utils.test.js new file mode 100644 index 0000000000..276f76ce47 --- /dev/null +++ b/src/export-page/utils.test.js @@ -0,0 +1,51 @@ +import Cookies from 'universal-cookie'; +import moment from 'moment'; + +import { LAST_EXPORT_COOKIE_NAME } from './data/constants'; +import { setExportCookie, getFormattedSuccessDate } from './utils'; + +global.window = Object.create(window); +Object.defineProperty(window, 'location', { + value: { + pathname: '/some-path', + }, +}); + +describe('setExportCookie', () => { + it('should set the export cookie with the provided date and completed status', () => { + const cookiesSetMock = jest.spyOn(Cookies.prototype, 'set'); + const date = '2023-07-24'; + const completed = true; + setExportCookie(date, completed); + + expect(cookiesSetMock).toHaveBeenCalledWith( + LAST_EXPORT_COOKIE_NAME, + { date, completed }, + { path: '/some-path' }, + ); + + cookiesSetMock.mockRestore(); + }); +}); + +describe('getFormattedSuccessDate', () => { + it('should return formatted success date with valid input', () => { + const mockCurrentUtcDate = moment.utc('2023-07-24T12:34:56'); + const momentMock = jest.spyOn(moment, 'utc').mockReturnValue(mockCurrentUtcDate); + + const unixDate = 1679787000; + + const expectedFormattedDate = ' (01/20/1970 at 10:36 UTC)'; + + const result = getFormattedSuccessDate(unixDate); + expect(result).toBe(expectedFormattedDate); + + momentMock.mockRestore(); + }); + + it('should return null with null input', () => { + const unixDate = null; + const result = getFormattedSuccessDate(unixDate); + expect(result).toBeNull(); + }); +}); diff --git a/src/generic/course-stepper/CourseStepper.test.jsx b/src/generic/course-stepper/CourseStepper.test.jsx new file mode 100644 index 0000000000..5a1c8dbe75 --- /dev/null +++ b/src/generic/course-stepper/CourseStepper.test.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import CourseStepper from '.'; + +const stepsMock = [ + { + title: 'Preparing', + description: 'Preparing to start the export', + }, + { + title: 'Exporting', + description: 'Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete', + }, + { + title: 'Compressing', + description: 'Compressing the exported data and preparing it for download', + }, + { + title: 'Success', + description: 'Your exported course can now be downloaded', + }, +]; + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('renders CourseStepper correctly', () => { + const { + getByText, getByTestId, getAllByTestId, queryByTestId, + } = renderComponent({ activeKey: 0 }); + + const steps = getAllByTestId('course-stepper__step'); + expect(steps.length).toBe(stepsMock.length); + + stepsMock.forEach((step) => { + expect(getByText(step.title)).toBeInTheDocument(); + expect(getByText(step.description)).toBeInTheDocument(); + expect(getByTestId(`${step.title}-icon`)).toBeInTheDocument(); + }); + + const percentElement = queryByTestId('course-stepper__step-percent'); + expect(percentElement).toBeNull(); + }); + + it('marks the active and done steps correctly', () => { + const activeKey = 1; + const { getAllByTestId } = renderComponent({ activeKey }); + + const steps = getAllByTestId('course-stepper__step'); + stepsMock.forEach((_, index) => { + const stepElement = steps[index]; + if (index === activeKey) { + expect(stepElement).toHaveClass('active'); + expect(stepElement).not.toHaveClass('done'); + } + if (index < activeKey) { + expect(stepElement).not.toHaveClass('active'); + expect(stepElement).toHaveClass('done'); + } + if (index > activeKey) { + expect(stepElement).not.toHaveClass('active'); + expect(stepElement).not.toHaveClass('done'); + } + }); + }); + + it('mark the error step correctly', () => { + const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true }); + + const errorStep = getAllByTestId('course-stepper__step')[1]; + expect(errorStep).toHaveClass('error'); + }); + + it('shows error message for error step', () => { + const errorMessage = 'Some error text'; + const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true, errorMessage }); + + const errorStep = getAllByTestId('course-stepper__step')[1]; + expect(errorStep).toHaveClass('error'); + }); + + it('shows percentage for active step', () => { + const percent = 50; + const { getByTestId } = renderComponent({ activeKey: 1, percent }); + + const percentElement = getByTestId('course-stepper__step-percent'); + expect(percentElement).toBeInTheDocument(); + expect(percentElement).toHaveTextContent(`${percent}%`); + }); + + it('shows null when steps length equal to zero', () => { + const { queryByTestId } = render( + + + , + ); + + const steps = queryByTestId('[data-testid="course-stepper__step"]'); + expect(steps).toBe(null); + }); +}); diff --git a/src/generic/course-stepper/CouseStepper.scss b/src/generic/course-stepper/CouseStepper.scss new file mode 100644 index 0000000000..868f1c6f89 --- /dev/null +++ b/src/generic/course-stepper/CouseStepper.scss @@ -0,0 +1,68 @@ +.course-stepper { + .course-stepper__step { + display: flex; + gap: $spacer; + padding: 1.25rem 0; + opacity: .5; + + &:not(:last-child) { + border-bottom: 1px solid $gray-200; + } + + .course-stepper__step-icon { + position: relative; + + & svg { + position: absolute; + top: .875rem; + left: 1.25rem; + } + } + + .course-stepper__step-info { + margin-left: 1.875rem; + } + + .course-stepper__step-title { + margin-bottom: .25rem; + } + + .course-stepper__step-percent { + margin: 0; + font-size: 1rem; + } + + .course-stepper__step-description { + margin: 0; + font-size: 1rem; + color: $gray-400; + } + } + + .course-stepper__step.active { + opacity: 1; + + & svg { + animation: rotate 2s infinite linear; + } + } + + .course-stepper__step.done { + opacity: 1; + + & svg, + .course-stepper__step-title { + color: $success-500; + } + } + + .course-stepper__step.error { + opacity: 1; + + .course-stepper__step-title, + .course-stepper__step-description, + & svg { + color: $danger-300; + } + } +} diff --git a/src/generic/course-stepper/index.jsx b/src/generic/course-stepper/index.jsx new file mode 100644 index 0000000000..01a4c4e220 --- /dev/null +++ b/src/generic/course-stepper/index.jsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { injectIntl } from '@edx/frontend-platform/i18n'; +import { + Settings as SettingsIcon, + CheckBoxOutlineBlank as SuccessIcon, + LibraryAddCheck as SuccessDoneIcon, + Warning as ErrorIcon, +} from '@edx/paragon/icons'; +import { Icon } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const CourseStepper = ({ + steps, + activeKey, + percent, + hasError, + errorMessage, +}) => { + const getStepperSettings = (index) => { + const lastStepIndex = steps.length - 1; + const isActiveStep = index === activeKey; + const isLastStep = index === lastStepIndex; + const isErrorStep = isActiveStep && hasError; + const isLastStepDone = isLastStep && isActiveStep; + + const getStepIcon = () => { + if (hasError && isActiveStep) { + return ErrorIcon; + } + if (isLastStep && !isActiveStep) { + return SuccessIcon; + } + if (isLastStepDone) { + return SuccessDoneIcon; + } + + return SettingsIcon; + }; + + return { + stepIcon: getStepIcon(index), + isPercentShow: Boolean(percent) && percent !== 100 && isActiveStep && !hasError, + isErrorMessageShow: isErrorStep && errorMessage, + isActiveClass: isActiveStep && !isLastStep && !hasError, + isDoneClass: index < activeKey || isLastStepDone, + isErrorClass: isErrorStep, + }; + }; + + return ( +
+ {steps.length ? steps.map(({ title, description }, index) => { + const { + stepIcon, + isPercentShow, + isErrorMessageShow, + isActiveClass, + isDoneClass, + isErrorClass, + } = getStepperSettings(index); + + return ( +
+
+ +
+
+

{title}

+ {isPercentShow && ( +

+ {percent}% +

+ )} +

+ {isErrorMessageShow ? errorMessage : description} +

+
+
+ ); + }) : null} +
+ ); +}; + +CourseStepper.defaultProps = { + percent: false, + hasError: false, + errorMessage: '', +}; + +CourseStepper.propTypes = { + steps: PropTypes.arrayOf(PropTypes.shape({ + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + })).isRequired, + activeKey: PropTypes.number.isRequired, + percent: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), + errorMessage: PropTypes.string, + hasError: PropTypes.bool, +}; + +export default injectIntl(CourseStepper); diff --git a/src/generic/modal-error/ModalError.jsx b/src/generic/modal-error/ModalError.jsx new file mode 100644 index 0000000000..b28d3a634b --- /dev/null +++ b/src/generic/modal-error/ModalError.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ActionRow, AlertModal, Button } from '@edx/paragon'; + +const ModalError = ({ + isOpen, title, message, handleCancel, handleAction, cancelButtonText, actionButtonText, +}) => ( + + + + + )} + > +

{message}

+
+); + +ModalError.propTypes = { + isOpen: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + handleCancel: PropTypes.func.isRequired, + handleAction: PropTypes.func.isRequired, + cancelButtonText: PropTypes.string.isRequired, + actionButtonText: PropTypes.string.isRequired, +}; + +export default ModalError; diff --git a/src/generic/styles.scss b/src/generic/styles.scss index 4eceeddce2..a8cacf01d4 100644 --- a/src/generic/styles.scss +++ b/src/generic/styles.scss @@ -4,3 +4,4 @@ @import "./section-sub-header/SectionSubHeader"; @import "./processing-notification/ProccessingNotification"; @import "./WysiwygEditor"; +@import "./course-stepper/CouseStepper"; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 3911a9ecb9..978e71536b 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -694,5 +694,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index caf3630337..6c093ff695 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index e53aaa9a20..31ae13b79d 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index 527bb5ceda..a2613cbcb1 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 15f9884a18..aaf6a32f4c 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index ada6d7c633..94e7e32789 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index caf3630337..6c093ff695 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index caf3630337..6c093ff695 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index 10f9cf32d5..34311b5f74 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index caf3630337..6c093ff695 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index cbc300f17e..a1c44f4087 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 61fb96195d..d8ffe39366 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -731,5 +731,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index 06fa663b51..891d1ef162 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index caf3630337..6c093ff695 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -695,5 +695,56 @@ "course-authoring.grading-settings.assignment.alert.warning.usage.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.warning.title": "Warning: The number of {type} assignments defined here does not match the current number of {type} assignments in the course:", "course-authoring.grading-settings.assignment.alert.success.title": "The number of {type} assignments in the course matches the number defined here.", - "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}" + "course-authoring.grading-settings.assignment.type-name.error.message-2": "For grading to work, you must change all {initialAssignmentName} subsections to {value}", + "course-authoring.export.footer.exportedData.title": "Data exported with your course:", + "course-authoring.export.footer.exportedData.item.1": "Values from Advanced settings, including MATLAB API keys and LTI passports", + "course-authoring.export.footer.exportedData.item.2": "Course content (all sections, sub-sections, and units)", + "course-authoring.export.footer.exportedData.item.3": "Course structure", + "course-authoring.export.footer.exportedData.item.4": "Individual problems", + "course-authoring.export.footer.exportedData.item.5": "Pages", + "course-authoring.export.footer.exportedData.item.6": "Course assets", + "course-authoring.export.footer.exportedData.item.7": "Course settings", + "course-authoring.export.footer.notExportedData.title": "Data not exported with your course:", + "course-authoring.export.footer.notExportedData.item.1": "User data", + "course-authoring.export.footer.notExportedData.item.2": "Course team data", + "course-authoring.export.footer.notExportedData.item.3": "Forum/discussion data", + "course-authoring.export.footer.notExportedData.item.4": "Certificates", + "course-authoring.export.modal.error.title": "There has been an error while exporting.", + "course-authoring.export.modal.error.description.not.unit": "Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.description.unit": "There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}", + "course-authoring.export.modal.error.button.cancel.unit": "Return to Export", + "course-authoring.export.modal.error.button.cancel.not.unit": "Cancel", + "course-authoring.export.modal.error.button.action.not.unit": "Take me to the main course page", + "course-authoring.export.modal.error.button.action.unit": "Correct failed component", + "course-authoring.export.sidebar.title1": "Why export a course?", + "course-authoring.export.sidebar.description1": "You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.", + "course-authoring.export.sidebar.exportedContent": "What content is exported?", + "course-authoring.export.sidebar.exportedContentHeading": "The following content is exported.", + "course-authoring.export.sidebar.content1": "Course content and structure", + "course-authoring.export.sidebar.content2": "Course dates", + "course-authoring.export.sidebar.content3": "Grading policy", + "course-authoring.export.sidebar.content4": "Any group configurations", + "course-authoring.export.sidebar.content5": "Settings on the Advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.export.sidebar.notExportedContent": "The following content is not exported.", + "course-authoring.export.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.export.sidebar.content7": "The course team", + "course-authoring.export.sidebar.openDownloadFile": "Opening the downloaded file", + "course-authoring.export.sidebar.openDownloadFileDescription": "Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.", + "course-authoring.export.sidebar.learnMoreButtonTitle": "Learn more about exporting a course", + "course-authoring.export.stepper.title.preparing": "Preparing", + "course-authoring.export.stepper.title.exporting": "Exporting", + "course-authoring.export.stepper.title.compressing": "Compressing", + "course-authoring.export.stepper.title.success": "Success", + "course-authoring.export.stepper.description.preparing": "Preparing to start the export", + "course-authoring.export.stepper.description.exporting": "Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)", + "course-authoring.export.stepper.description.compressing": "Compressing the exported data and preparing it for download", + "course-authoring.export.stepper.description.success": "Your exported course can now be downloaded", + "course-authoring.export.stepper.download.button.title": "Download exported course", + "course-authoring.export.stepper.header.title": "Course import status", + "course-authoring.export.heading.title": "Course export", + "course-authoring.export.heading.subtitle": "Tools", + "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" } diff --git a/src/index.scss b/src/index.scss index 6af8bf24d9..69f7b94c19 100755 --- a/src/index.scss +++ b/src/index.scss @@ -17,3 +17,4 @@ @import "pages-and-resources/PagesAndResources"; @import "course-team/CourseTeam"; @import "course-updates/CourseUpdates"; +@import "export-page/CourseExportPage"; diff --git a/src/store.js b/src/store.js index 5b538134a5..38a373ade4 100644 --- a/src/store.js +++ b/src/store.js @@ -14,6 +14,7 @@ import { reducer as courseTeamReducer } from './course-team/data/slice'; import { reducer as CourseUpdatesReducer } from './course-updates/data/slice'; import { reducer as processingNotificationReducer } from './generic/processing-notification/data/slice'; import { reducer as helpUrlsReducer } from './help-urls/data/slice'; +import { reducer as courseExportReducer } from './export-page/data/slice'; export default function initializeStore(preloadedState = undefined) { return configureStore({ @@ -32,6 +33,7 @@ export default function initializeStore(preloadedState = undefined) { courseUpdates: CourseUpdatesReducer, processingNotification: processingNotificationReducer, helpUrls: helpUrlsReducer, + courseExport: courseExportReducer, }, preloadedState, });