From 2ea876ae4fa6aa41821cec3ec5ed9402fa8463cb Mon Sep 17 00:00:00 2001 From: Kyrylo Kholodenko Date: Mon, 25 Sep 2023 19:07:08 +0300 Subject: [PATCH] feat: implement import page (#587) --- .env | 1 - .env.development | 1 - .env.test | 1 - src/CourseAuthoringRoutes.jsx | 6 +- src/i18n/messages/ar.json | 36 +++++- src/i18n/messages/de.json | 36 +++++- src/i18n/messages/de_DE.json | 36 +++++- src/i18n/messages/es_419.json | 36 +++++- src/i18n/messages/fr.json | 36 +++++- src/i18n/messages/fr_CA.json | 36 +++++- src/i18n/messages/hi.json | 36 +++++- src/i18n/messages/it.json | 36 +++++- src/i18n/messages/it_IT.json | 36 +++++- src/i18n/messages/pt.json | 36 +++++- src/i18n/messages/pt_PT.json | 36 +++++- src/i18n/messages/ru.json | 36 +++++- src/i18n/messages/uk.json | 36 +++++- src/i18n/messages/zh_CN.json | 36 +++++- src/import-page/CourseImportPage.jsx | 103 ++++++++++++++++ src/import-page/CourseImportPage.scss | 7 ++ src/import-page/CourseImportPage.test.jsx | 111 ++++++++++++++++++ src/import-page/data/api.js | 32 +++++ src/import-page/data/api.test.jsx | 48 ++++++++ src/import-page/data/constants.js | 8 ++ src/import-page/data/selectors.js | 8 ++ src/import-page/data/slice.js | 63 ++++++++++ src/import-page/data/thunks.js | 60 ++++++++++ src/import-page/file-section/FileSection.jsx | 64 ++++++++++ .../file-section/FileSection.test.jsx | 61 ++++++++++ src/import-page/file-section/messages.js | 14 +++ .../import-sidebar/ImportSidebar.jsx | 57 +++++++++ .../import-sidebar/ImportSidebar.scss | 4 + .../import-sidebar/ImportSidebar.test.jsx | 39 ++++++ src/import-page/import-sidebar/messages.js | 66 +++++++++++ .../import-stepper/ImportStepper.jsx | 103 ++++++++++++++++ .../import-stepper/ImportStepper.test.jsx | 38 ++++++ src/import-page/import-stepper/messages.js | 58 +++++++++ src/import-page/messages.js | 30 +++++ src/import-page/utils.js | 17 +++ src/import-page/utils.test.js | 29 +++++ src/index.scss | 1 + src/store.js | 2 + 42 files changed, 1515 insertions(+), 21 deletions(-) create mode 100644 src/import-page/CourseImportPage.jsx create mode 100644 src/import-page/CourseImportPage.scss create mode 100644 src/import-page/CourseImportPage.test.jsx create mode 100644 src/import-page/data/api.js create mode 100644 src/import-page/data/api.test.jsx create mode 100644 src/import-page/data/constants.js create mode 100644 src/import-page/data/selectors.js create mode 100644 src/import-page/data/slice.js create mode 100644 src/import-page/data/thunks.js create mode 100644 src/import-page/file-section/FileSection.jsx create mode 100644 src/import-page/file-section/FileSection.test.jsx create mode 100644 src/import-page/file-section/messages.js create mode 100644 src/import-page/import-sidebar/ImportSidebar.jsx create mode 100644 src/import-page/import-sidebar/ImportSidebar.scss create mode 100644 src/import-page/import-sidebar/ImportSidebar.test.jsx create mode 100644 src/import-page/import-sidebar/messages.js create mode 100644 src/import-page/import-stepper/ImportStepper.jsx create mode 100644 src/import-page/import-stepper/ImportStepper.test.jsx create mode 100644 src/import-page/import-stepper/messages.js create mode 100644 src/import-page/messages.js create mode 100644 src/import-page/utils.js create mode 100644 src/import-page/utils.test.js diff --git a/.env b/.env index 3df6415dc..2e9264c07 100644 --- a/.env +++ b/.env @@ -35,7 +35,6 @@ ENABLE_NEW_COURSE_OUTLINE_PAGE = false ENABLE_NEW_VIDEO_UPLOAD_PAGE = false ENABLE_NEW_GRADING_PAGE = false ENABLE_NEW_COURSE_TEAM_PAGE = false -ENABLE_NEW_IMPORT_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 8001ff8f5..4707975eb 100644 --- a/.env.development +++ b/.env.development @@ -36,7 +36,6 @@ ENABLE_NEW_COURSE_OUTLINE_PAGE = false ENABLE_NEW_VIDEO_UPLOAD_PAGE = false ENABLE_NEW_GRADING_PAGE = false ENABLE_NEW_COURSE_TEAM_PAGE = false -ENABLE_NEW_IMPORT_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 811825848..ecb87d55e 100644 --- a/.env.test +++ b/.env.test @@ -33,7 +33,6 @@ ENABLE_NEW_COURSE_OUTLINE_PAGE = true ENABLE_NEW_VIDEO_UPLOAD_PAGE = true ENABLE_NEW_GRADING_PAGE = true ENABLE_NEW_COURSE_TEAM_PAGE = true -ENABLE_NEW_IMPORT_PAGE = true ENABLE_UNIT_PAGE = true ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = true BBB_LEARN_MORE_URL='' diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index 3af7b0e74..05ddec53d 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -16,6 +16,7 @@ import { GradingSettings } from './grading-settings'; import CourseTeam from './course-team/CourseTeam'; import { CourseUpdates } from './course-updates'; import CourseExportPage from './export-page/CourseExportPage'; +import CourseImportPage from './import-page/CourseImportPage'; /** * As of this writing, these routes are mounted at a path prefixed with the following: @@ -100,10 +101,7 @@ const CourseAuthoringRoutes = ({ courseId }) => { - {process.env.ENABLE_NEW_IMPORT_PAGE === 'true' - && ( - - )} + diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index c9709852f..022419582 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -836,5 +836,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index e53c01750..d826cba46 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index 6b8493323..ab9afbf6a 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index adfc90d54..1bde58ad0 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 98303f4e3..6186a716f 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index e22568d6a..c94e8f435 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index e53c01750..d826cba46 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index e53c01750..d826cba46 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index b511ac020..615f9c9a4 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index e53c01750..d826cba46 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index f49865b28..1c1d4faf6 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 5bb50b7a9..58a473499 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -873,5 +873,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index f81185484..9841b3e46 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index e53c01750..d826cba46 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -837,5 +837,39 @@ "course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.", "course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?", "course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.", - "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs" + "course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs", + "course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content", + "course-authoring.import.file-section.chosen-file": "File chosen: {fileName}", + "course-authoring.import.sidebar.title1": "Why import a course?", + "course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.", + "course-authoring.import.sidebar.importedContent": "What content is imported?", + "course-authoring.import.sidebar.importedContentHeading": "The following content is imported.", + "course-authoring.import.sidebar.content1": "Course content and structure", + "course-authoring.import.sidebar.content2": "Course dates", + "course-authoring.import.sidebar.content3": "Grading policy", + "course-authoring.import.sidebar.content4": "Any group configurations", + "course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports", + "course-authoring.import.sidebar.notImportedContent": "The following content is not exported.", + "course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data", + "course-authoring.import.sidebar.content7": "The course team", + "course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running", + "course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.", + "course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course", + "course-authoring.import.stepper.title.uploading": "Uploading", + "course-authoring.import.stepper.title.unpacking": "Unpacking", + "course-authoring.import.stepper.title.verifying": "Verifying", + "course-authoring.import.stepper.title.updating": "Updating сourse", + "course-authoring.import.stepper.title.success": "Success", + "course-authoring.import.stepper.description.uploading": "Transferring your file to our servers", + "course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)", + "course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data", + "course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.", + "course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course", + "course-authoring.import.stepper.button.outline": "View updated outline", + "course-authoring.import.stepper.error.default": "Error importing course", + "course-authoring.import.heading.title": "Course import", + "course-authoring.import.heading.subtitle": "Tools", + "course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.", + "course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.", + "course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed." } diff --git a/src/import-page/CourseImportPage.jsx b/src/import-page/CourseImportPage.jsx new file mode 100644 index 000000000..997b5b975 --- /dev/null +++ b/src/import-page/CourseImportPage.jsx @@ -0,0 +1,103 @@ +/* eslint-disable max-len */ +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, +} from '@edx/paragon'; +import Cookies from 'universal-cookie'; +import { Helmet } from 'react-helmet'; + +import SubHeader from '../generic/sub-header/SubHeader'; +import InternetConnectionAlert from '../generic/internet-connection-alert'; +import { RequestStatus } from '../data/constants'; +import { useModel } from '../generic/model-store'; +import { + updateFileName, updateImportTriggered, updateSavingStatus, updateSuccessDate, +} from './data/slice'; +import ImportStepper from './import-stepper/ImportStepper'; +import { getImportTriggered, getLoadingStatus, getSavingStatus } from './data/selectors'; +import { LAST_IMPORT_COOKIE_NAME } from './data/constants'; +import ImportSidebar from './import-sidebar/ImportSidebar'; +import FileSection from './file-section/FileSection'; +import messages from './messages'; + +const CourseImportPage = ({ intl, courseId }) => { + const dispatch = useDispatch(); + const cookies = new Cookies(); + const courseDetails = useModel('courseDetails', courseId); + const importTriggered = useSelector(getImportTriggered); + const savingStatus = useSelector(getSavingStatus); + const loadingStatus = useSelector(getLoadingStatus); + const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED; + const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS; + + useEffect(() => { + const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME); + if (cookieData) { + dispatch(updateSavingStatus(RequestStatus.SUCCESSFUL)); + dispatch(updateImportTriggered(true)); + dispatch(updateFileName(cookieData.fileName)); + 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)}

+

{intl.formatMessage(messages.description2)}

+

{intl.formatMessage(messages.description3)}

+ + {importTriggered && } +
+
+ + + +
+
+
+
+ null} + /> +
+ + ); +}; + +CourseImportPage.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +CourseImportPage.defaultProps = {}; + +export default injectIntl(CourseImportPage); diff --git a/src/import-page/CourseImportPage.scss b/src/import-page/CourseImportPage.scss new file mode 100644 index 000000000..35ddbfba2 --- /dev/null +++ b/src/import-page/CourseImportPage.scss @@ -0,0 +1,7 @@ +@import "./import-sidebar/ImportSidebar"; + +.import { + .help-sidebar { + margin-top: 7.188rem; + } +} diff --git a/src/import-page/CourseImportPage.test.jsx b/src/import-page/CourseImportPage.test.jsx new file mode 100644 index 000000000..835931269 --- /dev/null +++ b/src/import-page/CourseImportPage.test.jsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { render, waitFor } from '@testing-library/react'; +import { Helmet } from 'react-helmet'; + +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import Cookies from 'universal-cookie'; +import initializeStore from '../store'; +import messages from './messages'; +import CourseImportPage from './CourseImportPage'; +import { getImportStatusApiUrl } from './data/api'; +import { IMPORT_STAGES } from './data/constants'; +import stepperMessages from './import-stepper/messages'; + +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 Cookie = { + get: jest.fn(), + set: jest.fn(), + }; + return jest.fn(() => Cookie); +}); + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getImportStatusApiUrl(courseId, 'testFileName.test')) + .reply(200, { importStatus: 1, message: '' }); + 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 importPageElement = getByText(messages.headingTitle.defaultMessage, { + selector: 'h2.sub-header-title', + }); + expect(importPageElement).toBeInTheDocument(); + expect(getByText(messages.description1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument(); + }); + }); + it('should fetch status without clicking when cookies has', async () => { + cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.test' }); + const { getByText } = render(); + expect(getByText(stepperMessages.stepperUnpackingDescription.defaultMessage)).toBeInTheDocument(); + }); + it('should show error', async () => { + axiosMock + .onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz')) + .reply(200, { importStatus: -IMPORT_STAGES.UPDATING, message: '' }); + cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' }); + const { getByText } = render(); + // eslint-disable-next-line no-promise-executor-return + await new Promise((r) => setTimeout(r, 3500)); + expect(getByText(stepperMessages.defaultErrorMessage.defaultMessage)).toBeInTheDocument(); + }); + it('should show success button', async () => { + axiosMock + .onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz')) + .reply(200, { importStatus: IMPORT_STAGES.SUCCESS, message: '' }); + cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' }); + const { getByText } = render(); + // eslint-disable-next-line no-promise-executor-return + await new Promise((r) => setTimeout(r, 3500)); + expect(getByText(stepperMessages.viewOutlineButton.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/import-page/data/api.js b/src/import-page/data/api.js new file mode 100644 index 000000000..17c68ab63 --- /dev/null +++ b/src/import-page/data/api.js @@ -0,0 +1,32 @@ +/* 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 postImportCourseApiUrl = (courseId) => `${getApiBaseUrl()}/import/${courseId}`; +export const getImportStatusApiUrl = (courseId, fileName) => `${getApiBaseUrl()}/import_status/${courseId}/${fileName}`; + +/** + * Start import course. + * @param {string} courseId + * @param {Object} fileData + * @param {Object} requestConfig + * @returns {Promise} + */ +export async function startCourseImporting(courseId, fileData, requestConfig) { + const { data } = await getAuthenticatedHttpClient() + .post(postImportCourseApiUrl(courseId), { 'course-data': fileData }, { headers: { 'content-type': 'multipart/form-data' }, ...requestConfig }); + return camelCaseObject(data); +} + +/** + * Get import status. + * @param {string} courseId + * @param {string} fileName + * @returns {Promise} + */ +export async function getImportStatus(courseId, fileName) { + const { data } = await getAuthenticatedHttpClient() + .get(getImportStatusApiUrl(courseId, fileName)); + return camelCaseObject(data); +} diff --git a/src/import-page/data/api.test.jsx b/src/import-page/data/api.test.jsx new file mode 100644 index 000000000..d0fd63d06 --- /dev/null +++ b/src/import-page/data/api.test.jsx @@ -0,0 +1,48 @@ +import MockAdapter from 'axios-mock-adapter'; +import { initializeMockApp, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { getImportStatus, postImportCourseApiUrl, startCourseImporting } 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 importing', async () => { + const data = { importStatus: 1 }; + axiosMock.onPost(postImportCourseApiUrl(courseId)).reply(200, data); + + const result = await startCourseImporting(courseId); + + expect(axiosMock.history.post[0].url).toEqual(postImportCourseApiUrl(courseId)); + expect(result).toEqual(data); + }); + + it('should fetch on get import status', async () => { + const data = { importStatus: 2 }; + const fileName = 'testFileName.test'; + const queryUrl = new URL(`import_status/${courseId}/${fileName}`, getConfig().STUDIO_BASE_URL).href; + axiosMock.onGet(queryUrl).reply(200, data); + + const result = await getImportStatus(courseId, fileName); + + expect(axiosMock.history.get[0].url).toEqual(queryUrl); + expect(result).toEqual(data); + }); +}); diff --git a/src/import-page/data/constants.js b/src/import-page/data/constants.js new file mode 100644 index 000000000..34a13ae78 --- /dev/null +++ b/src/import-page/data/constants.js @@ -0,0 +1,8 @@ +export const LAST_IMPORT_COOKIE_NAME = 'lastimport'; +export const IMPORT_STAGES = { + UPLOADING: 0, + UNPACKING: 1, + VERIFYING: 2, + UPDATING: 3, + SUCCESS: 4, +}; diff --git a/src/import-page/data/selectors.js b/src/import-page/data/selectors.js new file mode 100644 index 000000000..807566daa --- /dev/null +++ b/src/import-page/data/selectors.js @@ -0,0 +1,8 @@ +export const getProgress = (state) => state.courseImport.progress; +export const getCurrentStage = (state) => state.courseImport.currentStage; +export const getImportTriggered = (state) => state.courseImport.importTriggered; +export const getFileName = (state) => state.courseImport.fileName; +export const getError = (state) => state.courseImport.error; +export const getLoadingStatus = (state) => state.courseImport.loadingStatus; +export const getSavingStatus = (state) => state.courseImport.savingStatus; +export const getSuccessDate = (state) => state.courseImport.successDate; diff --git a/src/import-page/data/slice.js b/src/import-page/data/slice.js new file mode 100644 index 000000000..82550e705 --- /dev/null +++ b/src/import-page/data/slice.js @@ -0,0 +1,63 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + currentStage: 0, + error: { hasError: false, message: '' }, + progress: 0, + importTriggered: false, + fileName: null, + loadingStatus: '', + savingStatus: '', + successDate: null, +}; + +const slice = createSlice({ + name: 'importPage', + initialState, + reducers: { + updateCurrentStage: (state, { payload }) => { + if (payload >= state.currentStage) { + state.currentStage = payload; + } + }, + updateError: (state, { payload }) => { + state.error = { ...state.error, ...payload }; + }, + updateProgress: (state, { payload }) => { + state.progress = payload; + }, + updateImportTriggered: (state, { payload }) => { + state.importTriggered = payload; + }, + updateFileName: (state, { payload }) => { + state.fileName = payload; + }, + reset: () => initialState, + updateLoadingStatus: (state, { payload }) => { + state.loadingStatus = payload; + }, + updateSavingStatus: (state, { payload }) => { + state.savingStatus = payload; + }, + updateSuccessDate: (state, { payload }) => { + state.successDate = payload; + }, + }, +}); + +export const { + updateCurrentStage, + updateError, + updateProgress, + updateImportTriggered, + updateFileName, + reset, + updateLoadingStatus, + updateSavingStatus, + updateSuccessDate, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/import-page/data/thunks.js b/src/import-page/data/thunks.js new file mode 100644 index 000000000..04415443f --- /dev/null +++ b/src/import-page/data/thunks.js @@ -0,0 +1,60 @@ +/* eslint-disable import/prefer-default-export */ +import Cookies from 'universal-cookie'; +import moment from 'moment'; + +import { RequestStatus } from '../../data/constants'; +import { setImportCookie } from '../utils'; +import { getImportStatus, startCourseImporting } from './api'; +import { + reset, updateCurrentStage, updateError, updateFileName, + updateImportTriggered, updateLoadingStatus, updateSavingStatus, updateSuccessDate, +} from './slice'; +import { IMPORT_STAGES, LAST_IMPORT_COOKIE_NAME } from './constants'; + +export function fetchImportStatus(courseId, fileName) { + return async (dispatch) => { + try { + dispatch(updateLoadingStatus(RequestStatus.IN_PROGRESS)); + const { importStatus, message } = await getImportStatus(courseId, fileName); + dispatch(updateCurrentStage(Math.abs(importStatus))); + const cookies = new Cookies(); + const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME); + + if (importStatus < 0) { + dispatch(updateError({ hasError: true, message })); + } else if (importStatus === IMPORT_STAGES.SUCCESS && !cookieData?.completed) { + dispatch(updateSuccessDate(moment().valueOf())); + } + + if (!cookieData?.completed) { + setImportCookie(moment().valueOf(), importStatus === IMPORT_STAGES.SUCCESS, fileName); + } + dispatch(updateLoadingStatus(RequestStatus.SUCCESSFUL)); + return true; + } catch (error) { + dispatch(updateLoadingStatus(RequestStatus.FAILED)); + return false; + } + }; +} + +export function handleProcessUpload(courseId, fileData, requestConfig, handleError) { + return async (dispatch) => { + try { + const file = fileData.get('file'); + dispatch(reset()); + dispatch(updateSavingStatus(RequestStatus.PENDING)); + dispatch(updateImportTriggered(true)); + dispatch(updateFileName(file.name)); + const { importStatus } = await startCourseImporting(courseId, file, requestConfig); + dispatch(updateCurrentStage(importStatus)); + setImportCookie(moment().valueOf(), importStatus === IMPORT_STAGES.SUCCESS, file.name); + dispatch(updateSavingStatus(RequestStatus.SUCCESSFUL)); + return true; + } catch (error) { + handleError(error); + dispatch(updateSavingStatus(RequestStatus.FAILED)); + return false; + } + }; +} diff --git a/src/import-page/file-section/FileSection.jsx b/src/import-page/file-section/FileSection.jsx new file mode 100644 index 000000000..9f16502db --- /dev/null +++ b/src/import-page/file-section/FileSection.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { Card, Dropzone } from '@edx/paragon'; + +import { IMPORT_STAGES } from '../data/constants'; +import { + getCurrentStage, getError, getFileName, getImportTriggered, +} from '../data/selectors'; +import { updateProgress } from '../data/slice'; +import messages from './messages'; +import { handleProcessUpload } from '../data/thunks'; + +const FileSection = ({ intl, courseId }) => { + const dispatch = useDispatch(); + const importTriggered = useSelector(getImportTriggered); + const currentStage = useSelector(getCurrentStage); + const fileName = useSelector(getFileName); + const { hasError } = useSelector(getError); + const isShowedDropzone = !importTriggered || currentStage === IMPORT_STAGES.SUCCESS || hasError; + + return ( + + + + {isShowedDropzone + && ( + dispatch(handleProcessUpload( + courseId, + fileData, + requestConfig, + handleError, + )) + } + onUploadProgress={(percent) => dispatch(updateProgress(percent))} + accept={{ 'application/gzip': ['.tar.gz'] }} + data-testid="dropzone" + /> + )} + + {fileName && ( +
+ {intl.formatMessage(messages.fileChosen, { fileName })} +
+ )} +
+ ); +}; + +FileSection.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +export default injectIntl(FileSection); diff --git a/src/import-page/file-section/FileSection.test.jsx b/src/import-page/file-section/FileSection.test.jsx new file mode 100644 index 000000000..6d6bfb2d2 --- /dev/null +++ b/src/import-page/file-section/FileSection.test.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; + +import initializeStore from '../../store'; +import messages from './messages'; +import FileSection from './FileSection'; + +let store; +const courseId = '123'; + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + }); + it('should render without errors', async () => { + const { getByText } = render(); + await waitFor(() => { + expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); + }); + }); + it('should displays Dropzone when import is not triggered or in success stage or has an error', async () => { + const { getByTestId } = render(); + await waitFor(() => { + expect(getByTestId('dropzone')).toBeInTheDocument(); + }); + }); + it('should work Dropzone', async () => { + const { + getByText, getByTestId, queryByTestId, container, + } = render(); + + const dropzoneElement = getByTestId('dropzone'); + + const file = new File(['file contents'], 'example.tar.gz', { type: 'application/gzip' }); + fireEvent.drop(dropzoneElement, { dataTransfer: { files: [file], types: ['Files'] } }); + + await waitFor(() => { + expect(getByText('File chosen: example.tar.gz')).toBeInTheDocument(); + expect(queryByTestId(container, 'dropzone')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/import-page/file-section/messages.js b/src/import-page/file-section/messages.js new file mode 100644 index 000000000..6ad57b9a5 --- /dev/null +++ b/src/import-page/file-section/messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + headingTitle: { + id: 'course-authoring.import.file-section.title', + defaultMessage: 'Select a .tar.gz file to replace your course content', + }, + fileChosen: { + id: 'course-authoring.import.file-section.chosen-file', + defaultMessage: 'File chosen: {fileName}', + }, +}); + +export default messages; diff --git a/src/import-page/import-sidebar/ImportSidebar.jsx b/src/import-page/import-sidebar/ImportSidebar.jsx new file mode 100644 index 000000000..8287b3d60 --- /dev/null +++ b/src/import-page/import-sidebar/ImportSidebar.jsx @@ -0,0 +1,57 @@ +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 ImportSidebar = ({ intl, courseId }) => { + const { importCourse: importLearnMoreUrl } = useHelpUrls(['importCourse']); + return ( + +

{intl.formatMessage(messages.title1)}

+

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

+
+

{intl.formatMessage(messages.importedContent)}

+

{intl.formatMessage(messages.importedContentHeading)}

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

{intl.formatMessage(messages.notImportedContent)}

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

{intl.formatMessage(messages.warningTitle)}

+

{intl.formatMessage(messages.warningDescription)}

+
+ +
+ ); +}; + +ImportSidebar.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +export default injectIntl(ImportSidebar); diff --git a/src/import-page/import-sidebar/ImportSidebar.scss b/src/import-page/import-sidebar/ImportSidebar.scss new file mode 100644 index 000000000..7ac034418 --- /dev/null +++ b/src/import-page/import-sidebar/ImportSidebar.scss @@ -0,0 +1,4 @@ +.import-sidebar-list { + list-style: none; + padding-left: 0; +} diff --git a/src/import-page/import-sidebar/ImportSidebar.test.jsx b/src/import-page/import-sidebar/ImportSidebar.test.jsx new file mode 100644 index 000000000..1fb3c2e28 --- /dev/null +++ b/src/import-page/import-sidebar/ImportSidebar.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 ImportSidebar from './ImportSidebar'; + +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.importedContentHeading.defaultMessage)).toBeInTheDocument(); + }); +}); diff --git a/src/import-page/import-sidebar/messages.js b/src/import-page/import-sidebar/messages.js new file mode 100644 index 000000000..95e22cfd9 --- /dev/null +++ b/src/import-page/import-sidebar/messages.js @@ -0,0 +1,66 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + title1: { + id: 'course-authoring.import.sidebar.title1', + defaultMessage: 'Why import a course?', + }, + description1: { + id: 'course-authoring.import.sidebar.description1', + defaultMessage: 'You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.', + }, + importedContent: { + id: 'course-authoring.import.sidebar.importedContent', + defaultMessage: 'What content is imported?', + }, + importedContentHeading: { + id: 'course-authoring.import.sidebar.importedContentHeading', + defaultMessage: 'The following content is imported.', + }, + content1: { + id: 'course-authoring.import.sidebar.content1', + defaultMessage: 'Course content and structure', + }, + content2: { + id: 'course-authoring.import.sidebar.content2', + defaultMessage: 'Course dates', + }, + content3: { + id: 'course-authoring.import.sidebar.content3', + defaultMessage: 'Grading policy', + }, + content4: { + id: 'course-authoring.import.sidebar.content4', + defaultMessage: 'Any group configurations', + }, + content5: { + id: 'course-authoring.import.sidebar.content5', + defaultMessage: 'Settings on the advanced settings page, including MATLAB API keys and LTI passports', + }, + notImportedContent: { + id: 'course-authoring.import.sidebar.notImportedContent', + defaultMessage: 'The following content is not exported.', + }, + content6: { + id: 'course-authoring.import.sidebar.content6', + defaultMessage: 'Learner-specific content, such as learner grades and discussion forum data', + }, + content7: { + id: 'course-authoring.import.sidebar.content7', + defaultMessage: 'The course team', + }, + warningTitle: { + id: 'course-authoring.import.sidebar.warningTitle', + defaultMessage: 'Warning: importing while a course is running', + }, + warningDescription: { + id: 'course-authoring.import.sidebar.warningDescription', + defaultMessage: 'If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students\' problem scores.', + }, + learnMoreButtonTitle: { + id: 'course-authoring.import.sidebar.learnMoreButtonTitle', + defaultMessage: 'Learn more about importing a course', + }, +}); + +export default messages; diff --git a/src/import-page/import-stepper/ImportStepper.jsx b/src/import-page/import-stepper/ImportStepper.jsx new file mode 100644 index 000000000..e160ec625 --- /dev/null +++ b/src/import-page/import-stepper/ImportStepper.jsx @@ -0,0 +1,103 @@ +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 { Button } from '@edx/paragon'; +import { getConfig } from '@edx/frontend-platform'; + +import { getFormattedSuccessDate } from '../../export-page/utils'; +import { RequestStatus } from '../../data/constants'; +import CourseStepper from '../../generic/course-stepper'; +import { IMPORT_STAGES } from '../data/constants'; +import { fetchImportStatus } from '../data/thunks'; +import { + getCurrentStage, getError, getFileName, getLoadingStatus, getProgress, getSavingStatus, getSuccessDate, +} from '../data/selectors'; +import messages from './messages'; + +const ImportStepper = ({ intl, courseId }) => { + const currentStage = useSelector(getCurrentStage); + const fileName = useSelector(getFileName); + const { hasError, message: errorMessage } = useSelector(getError); + const progress = useSelector(getProgress); + const dispatch = useDispatch(); + const loadingStatus = useSelector(getLoadingStatus); + const savingStatus = useSelector(getSavingStatus); + const successDate = useSelector(getSuccessDate); + const isStopFetching = currentStage === IMPORT_STAGES.SUCCESS + || loadingStatus === RequestStatus.FAILED + || savingStatus === RequestStatus.FAILED + || hasError; + const formattedErrorMessage = hasError ? errorMessage || intl.formatMessage(messages.defaultErrorMessage) : ''; + + useEffect(() => { + const id = setInterval(() => { + if (isStopFetching) { + clearInterval(id); + } else if (fileName) { + dispatch(fetchImportStatus(courseId, fileName)); + } + }, 3000); + return () => clearInterval(id); + }); + + let successTitle = intl.formatMessage(messages.stepperSuccessTitle); + const formattedSuccessDate = getFormattedSuccessDate(successDate); + if (currentStage === IMPORT_STAGES.SUCCESS && formattedSuccessDate) { + successTitle += formattedSuccessDate; + } + + const handleRedirectCourseOutline = () => window.location.replace(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`); + + const steps = [ + { + title: intl.formatMessage(messages.stepperUploadingTitle), + description: intl.formatMessage(messages.stepperUploadingDescription), + key: IMPORT_STAGES.UPLOADING, + }, { + title: intl.formatMessage(messages.stepperUnpackingTitle), + description: intl.formatMessage(messages.stepperUnpackingDescription), + key: IMPORT_STAGES.UNPACKING, + }, { + title: intl.formatMessage(messages.stepperVerifyingTitle), + description: intl.formatMessage(messages.stepperVerifyingDescription), + key: IMPORT_STAGES.VERIFYING, + }, { + title: intl.formatMessage(messages.stepperUpdatingTitle), + description: intl.formatMessage(messages.stepperUpdatingDescription), + key: IMPORT_STAGES.UPDATING, + }, { + title: successTitle, + description: intl.formatMessage(messages.stepperSuccessDescription), + key: IMPORT_STAGES.SUCCESS, + }, + ]; + + return ( +
+

{intl.formatMessage(messages.stepperHeaderTitle)}

+
+ + {currentStage === IMPORT_STAGES.SUCCESS && ( + + )} +
+ ); +}; + +ImportStepper.propTypes = { + intl: intlShape.isRequired, + courseId: PropTypes.string.isRequired, +}; + +export default injectIntl(ImportStepper); diff --git a/src/import-page/import-stepper/ImportStepper.test.jsx b/src/import-page/import-stepper/ImportStepper.test.jsx new file mode 100644 index 000000000..eaf466da4 --- /dev/null +++ b/src/import-page/import-stepper/ImportStepper.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 ImportStepper from './ImportStepper'; + +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/import-page/import-stepper/messages.js b/src/import-page/import-stepper/messages.js new file mode 100644 index 000000000..83d720600 --- /dev/null +++ b/src/import-page/import-stepper/messages.js @@ -0,0 +1,58 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + stepperUploadingTitle: { + id: 'course-authoring.import.stepper.title.uploading', + defaultMessage: 'Uploading', + }, + stepperUnpackingTitle: { + id: 'course-authoring.import.stepper.title.unpacking', + defaultMessage: 'Unpacking', + }, + stepperVerifyingTitle: { + id: 'course-authoring.import.stepper.title.verifying', + defaultMessage: 'Verifying', + }, + stepperUpdatingTitle: { + id: 'course-authoring.import.stepper.title.updating', + defaultMessage: 'Updating сourse', + }, + stepperSuccessTitle: { + id: 'course-authoring.import.stepper.title.success', + defaultMessage: 'Success', + }, + stepperUploadingDescription: { + id: 'course-authoring.import.stepper.description.uploading', + defaultMessage: 'Transferring your file to our servers', + }, + stepperUnpackingDescription: { + id: 'course-authoring.import.stepper.description.unpacking', + defaultMessage: 'Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)', + }, + stepperVerifyingDescription: { + id: 'course-authoring.import.stepper.description.verifying', + defaultMessage: 'Reviewing semantics, syntax, and required data', + }, + stepperUpdatingDescription: { + id: 'course-authoring.import.stepper.description.updating', + defaultMessage: 'Integrating your imported content into this course. This process might take longer with larger courses.', + }, + stepperSuccessDescription: { + id: 'course-authoring.import.stepper.description.success', + defaultMessage: 'Your imported content has now been integrated into this course', + }, + viewOutlineButton: { + id: 'course-authoring.import.stepper.button.outline', + defaultMessage: 'View updated outline', + }, + defaultErrorMessage: { + id: 'course-authoring.import.stepper.error.default', + defaultMessage: 'Error importing course', + }, + stepperHeaderTitle: { + id: 'course-authoring.export.stepper.header.title', + defaultMessage: 'Course import status', + }, +}); + +export default messages; diff --git a/src/import-page/messages.js b/src/import-page/messages.js new file mode 100644 index 000000000..7b7c21371 --- /dev/null +++ b/src/import-page/messages.js @@ -0,0 +1,30 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + pageTitle: { + id: 'course-authoring.import.page.title', + defaultMessage: '{headingTitle} | {courseName} | {siteName}', + }, + headingTitle: { + id: 'course-authoring.import.heading.title', + defaultMessage: 'Course import', + }, + headingSubtitle: { + id: 'course-authoring.import.heading.subtitle', + defaultMessage: 'Tools', + }, + description1: { + id: 'course-authoring.import.description1', + defaultMessage: 'Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.', + }, + description2: { + id: 'course-authoring.import.description2', + defaultMessage: 'The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.', + }, + description3: { + id: 'course-authoring.import.description3', + defaultMessage: 'The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don\'t make important changes to your course until the import operation has completed.', + }, +}); + +export default messages; diff --git a/src/import-page/utils.js b/src/import-page/utils.js new file mode 100644 index 000000000..113e77ccc --- /dev/null +++ b/src/import-page/utils.js @@ -0,0 +1,17 @@ +import Cookies from 'universal-cookie'; + +import { LAST_IMPORT_COOKIE_NAME } from './data/constants'; + +/** + * Sets an import-related cookie with the provided information. + * + * @param {Date} date - Date of import. + * @param {boolean} completed - Indicates if import was completed successfully. + * @param {string} fileName - File name. + * @returns {void} + */ +// eslint-disable-next-line import/prefer-default-export +export const setImportCookie = (date, completed, fileName) => { + const cookies = new Cookies(); + cookies.set(LAST_IMPORT_COOKIE_NAME, { date, completed, fileName }, { path: window.location.pathname }); +}; diff --git a/src/import-page/utils.test.js b/src/import-page/utils.test.js new file mode 100644 index 000000000..5b3c55dbf --- /dev/null +++ b/src/import-page/utils.test.js @@ -0,0 +1,29 @@ +import Cookies from 'universal-cookie'; + +import { LAST_IMPORT_COOKIE_NAME } from './data/constants'; +import { setImportCookie } from './utils'; + +global.window = Object.create(window); +Object.defineProperty(window, 'location', { + value: { + pathname: '/some-path', + }, +}); + +describe('setImportCookie', () => { + it('should set the import cookie with the provided data', () => { + const cookiesSetMock = jest.spyOn(Cookies.prototype, 'set'); + const date = '2023-07-24'; + const completed = true; + const fileName = 'testFileName.test'; + setImportCookie(date, completed, fileName); + + expect(cookiesSetMock).toHaveBeenCalledWith( + LAST_IMPORT_COOKIE_NAME, + { date, completed, fileName }, + { path: '/some-path' }, + ); + + cookiesSetMock.mockRestore(); + }); +}); diff --git a/src/index.scss b/src/index.scss index b80bc1160..55dea0489 100755 --- a/src/index.scss +++ b/src/index.scss @@ -19,3 +19,4 @@ @import "course-team/CourseTeam"; @import "course-updates/CourseUpdates"; @import "export-page/CourseExportPage"; +@import "import-page/CourseImportPage"; diff --git a/src/store.js b/src/store.js index d2345a20d..4b88a6435 100644 --- a/src/store.js +++ b/src/store.js @@ -17,6 +17,7 @@ import { reducer as processingNotificationReducer } from './generic/processing-n import { reducer as helpUrlsReducer } from './help-urls/data/slice'; import { reducer as courseExportReducer } from './export-page/data/slice'; import { reducer as genericReducer } from './generic/data/slice'; +import { reducer as courseImportReducer } from './import-page/data/slice'; export default function initializeStore(preloadedState = undefined) { return configureStore({ @@ -38,6 +39,7 @@ export default function initializeStore(preloadedState = undefined) { helpUrls: helpUrlsReducer, courseExport: courseExportReducer, generic: genericReducer, + courseImport: courseImportReducer, }, preloadedState, });