Compare commits
39 Commits
refactor--
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292931f2cb | ||
|
|
a580b8980a | ||
|
|
4592c0d06a | ||
|
|
91922874fc | ||
|
|
0020597d97 | ||
|
|
74ce163bec | ||
|
|
2b8edfd761 | ||
|
|
d0c4765698 | ||
|
|
d48d6ffa5d | ||
|
|
14f035435c | ||
|
|
fa25150e3d | ||
|
|
3fe35344f0 | ||
|
|
bbca5a29b7 | ||
|
|
2a6a816baf | ||
|
|
73f7d5d5f5 | ||
|
|
0871ce345a | ||
|
|
01ddac380f | ||
|
|
4840666664 | ||
|
|
21e4ece669 | ||
|
|
887a628c23 | ||
|
|
2ea876ae4f | ||
|
|
c47c800cfa | ||
|
|
ef9633af35 | ||
|
|
217b86e616 | ||
|
|
37aabc4948 | ||
|
|
e099243437 | ||
|
|
6f238bdbe0 | ||
|
|
77dfd0296c | ||
|
|
1888993113 | ||
|
|
fb28693854 | ||
|
|
7f8c6f2d61 | ||
|
|
15984473b4 | ||
|
|
b03ecf1562 | ||
|
|
fdc5916ada | ||
|
|
a54d351e9c | ||
|
|
62cde57556 | ||
|
|
2bd8037d7b | ||
|
|
a1793efcc0 | ||
|
|
ed2eed5110 |
6
.env
6
.env
@@ -23,6 +23,7 @@ PUBLISHER_BASE_URL=''
|
|||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME=''
|
SITE_NAME=''
|
||||||
|
STUDIO_SHORT_NAME='Studio'
|
||||||
SUPPORT_EMAIL=''
|
SUPPORT_EMAIL=''
|
||||||
SUPPORT_URL=''
|
SUPPORT_URL=''
|
||||||
USER_INFO_COOKIE_NAME=''
|
USER_INFO_COOKIE_NAME=''
|
||||||
@@ -30,13 +31,8 @@ ENABLE_ACCESSIBILITY_PAGE=false
|
|||||||
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||||
ENABLE_TEAM_TYPE_SETTING=false
|
ENABLE_TEAM_TYPE_SETTING=false
|
||||||
ENABLE_NEW_EDITOR_PAGES=true
|
ENABLE_NEW_EDITOR_PAGES=true
|
||||||
ENABLE_NEW_HOME_PAGE = false
|
|
||||||
ENABLE_NEW_COURSE_OUTLINE_PAGE = false
|
ENABLE_NEW_COURSE_OUTLINE_PAGE = false
|
||||||
ENABLE_NEW_VIDEO_UPLOAD_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_NEW_EXPORT_PAGE = false
|
|
||||||
ENABLE_UNIT_PAGE = false
|
ENABLE_UNIT_PAGE = false
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
|
||||||
BBB_LEARN_MORE_URL=''
|
BBB_LEARN_MORE_URL=''
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
|||||||
SEGMENT_KEY=null
|
SEGMENT_KEY=null
|
||||||
SITE_NAME='Your Plaform Name Here'
|
SITE_NAME='Your Plaform Name Here'
|
||||||
STUDIO_BASE_URL='http://localhost:18010'
|
STUDIO_BASE_URL='http://localhost:18010'
|
||||||
|
STUDIO_SHORT_NAME='Studio'
|
||||||
SUPPORT_EMAIL=
|
SUPPORT_EMAIL=
|
||||||
SUPPORT_URL='https://support.edx.org'
|
SUPPORT_URL='https://support.edx.org'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
@@ -32,13 +33,8 @@ ENABLE_ACCESSIBILITY_PAGE=false
|
|||||||
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||||
ENABLE_TEAM_TYPE_SETTING=false
|
ENABLE_TEAM_TYPE_SETTING=false
|
||||||
ENABLE_NEW_EDITOR_PAGES=true
|
ENABLE_NEW_EDITOR_PAGES=true
|
||||||
ENABLE_NEW_HOME_PAGE = false
|
|
||||||
ENABLE_NEW_COURSE_OUTLINE_PAGE = false
|
ENABLE_NEW_COURSE_OUTLINE_PAGE = false
|
||||||
ENABLE_NEW_VIDEO_UPLOAD_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_NEW_EXPORT_PAGE = false
|
|
||||||
ENABLE_UNIT_PAGE = false
|
ENABLE_UNIT_PAGE = false
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
|
||||||
BBB_LEARN_MORE_URL=''
|
BBB_LEARN_MORE_URL=''
|
||||||
|
|||||||
@@ -22,19 +22,15 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
|||||||
SEGMENT_KEY=null
|
SEGMENT_KEY=null
|
||||||
SITE_NAME='edX'
|
SITE_NAME='edX'
|
||||||
STUDIO_BASE_URL='http://localhost:18010'
|
STUDIO_BASE_URL='http://localhost:18010'
|
||||||
|
STUDIO_SHORT_NAME='Studio'
|
||||||
SUPPORT_EMAIL='support@example.com'
|
SUPPORT_EMAIL='support@example.com'
|
||||||
SUPPORT_URL='https://support.edx.org'
|
SUPPORT_URL='https://support.edx.org'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||||
ENABLE_TEAM_TYPE_SETTING=false
|
ENABLE_TEAM_TYPE_SETTING=false
|
||||||
ENABLE_NEW_EDITOR_PAGES=true
|
ENABLE_NEW_EDITOR_PAGES=true
|
||||||
ENABLE_NEW_HOME_PAGE = false
|
|
||||||
ENABLE_NEW_COURSE_OUTLINE_PAGE = true
|
ENABLE_NEW_COURSE_OUTLINE_PAGE = true
|
||||||
ENABLE_NEW_VIDEO_UPLOAD_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_NEW_EXPORT_PAGE = true
|
|
||||||
ENABLE_UNIT_PAGE = true
|
ENABLE_UNIT_PAGE = true
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = true
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = true
|
||||||
BBB_LEARN_MORE_URL=''
|
BBB_LEARN_MORE_URL=''
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ coverage:
|
|||||||
default:
|
default:
|
||||||
target: auto
|
target: auto
|
||||||
threshold: 0%
|
threshold: 0%
|
||||||
|
ignore:
|
||||||
|
- "src/grading-settings/grading-scale/react-ranger.js"
|
||||||
|
|||||||
1100
package-lock.json
generated
1100
package-lock.json
generated
@@ -10,9 +10,10 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||||
"@edx/frontend-component-footer": "12.0.0",
|
"@edx/frontend-component-footer": "^12.3.0",
|
||||||
|
"@edx/frontend-component-header": "^4.7.0",
|
||||||
"@edx/frontend-enterprise-hotjar": "^1.2.1",
|
"@edx/frontend-enterprise-hotjar": "^1.2.1",
|
||||||
"@edx/frontend-lib-content-components": "^1.169.3",
|
"@edx/frontend-lib-content-components": "^1.174.0",
|
||||||
"@edx/frontend-platform": "4.2.0",
|
"@edx/frontend-platform": "4.2.0",
|
||||||
"@edx/paragon": "^20.45.4",
|
"@edx/paragon": "^20.45.4",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"core-js": "3.8.1",
|
"core-js": "3.8.1",
|
||||||
"email-validator": "2.0.4",
|
"email-validator": "2.0.4",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"formik": "2.2.6",
|
"formik": "2.2.6",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
@@ -33,7 +35,6 @@
|
|||||||
"react-datepicker": "^4.13.0",
|
"react-datepicker": "^4.13.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-ranger": "^2.1.0",
|
|
||||||
"react-redux": "7.1.3",
|
"react-redux": "7.1.3",
|
||||||
"react-responsive": "8.1.0",
|
"react-responsive": "8.1.0",
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"react-transition-group": "4.4.1",
|
"react-transition-group": "4.4.1",
|
||||||
"redux": "4.0.5",
|
"redux": "4.0.5",
|
||||||
"regenerator-runtime": "0.13.7",
|
"regenerator-runtime": "0.13.7",
|
||||||
|
"universal-cookie": "^4.0.4",
|
||||||
"uuid": "^3.4.0",
|
"uuid": "^3.4.0",
|
||||||
"yup": "0.31.1"
|
"yup": "0.31.1"
|
||||||
},
|
},
|
||||||
@@ -2200,69 +2202,142 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-component-footer": {
|
"node_modules/@edx/frontend-component-footer": {
|
||||||
"version": "12.0.0",
|
"version": "12.3.0",
|
||||||
"license": "AGPL-3.0",
|
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.3.0.tgz",
|
||||||
|
"integrity": "sha512-ivCtioyP4SceYM4/ugVtif4c41Y+epA0NM7sSB/x6s9A/RTQXb2TY3fDc9lB3ah/0+pRwGVJJEVYkPAZ4JdC/g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
"@edx/paragon": "^21.3.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
"@fortawesome/free-brands-svg-icons": "6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0"
|
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||||
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@edx/frontend-platform": "^4.0.0",
|
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^16.9.0 || ^17.0.0",
|
"react": "^16.9.0 || ^17.0.0",
|
||||||
"react-dom": "^16.9.0 || ^17.0.0"
|
"react-dom": "^16.9.0 || ^17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/@edx/paragon": {
|
||||||
|
"version": "21.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-21.3.1.tgz",
|
||||||
|
"integrity": "sha512-bXTUaOEmT8XLnDQzYS8QLMvWK5K2BN4jHlx25lO8N0XWRQeDiQTdbx8OrEbv8QOPTlrv0an5MZc+qjlleJFObg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||||
|
"@popperjs/core": "^2.11.4",
|
||||||
|
"bootstrap": "^4.6.2",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"child_process": "^1.0.2",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"email-prop-type": "^3.0.0",
|
||||||
|
"file-selector": "^0.6.0",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"glob": "^8.0.3",
|
||||||
|
"inquirer": "^8.2.5",
|
||||||
|
"lodash.uniqby": "^4.7.0",
|
||||||
|
"mailto-link": "^2.0.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-bootstrap": "^1.6.5",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
|
"react-dropzone": "^14.2.1",
|
||||||
|
"react-focus-on": "^3.5.4",
|
||||||
|
"react-loading-skeleton": "^3.1.0",
|
||||||
|
"react-popper": "^2.2.5",
|
||||||
|
"react-proptype-conditional-require": "^1.0.4",
|
||||||
|
"react-responsive": "^8.2.0",
|
||||||
|
"react-table": "^7.7.0",
|
||||||
|
"react-transition-group": "^4.4.2",
|
||||||
|
"tabbable": "^5.3.3",
|
||||||
|
"uncontrollable": "^7.2.1",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"paragon": "bin/paragon-scripts.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.6 || ^17.0.0",
|
||||||
|
"react-dom": "^16.8.6 || ^17.0.0",
|
||||||
|
"react-intl": "^5.25.1 || ^6.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/@edx/paragon/node_modules/@fortawesome/react-fontawesome": {
|
||||||
|
"version": "0.1.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
|
||||||
|
"integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||||
|
"react": ">=16.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/@edx/paragon/node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": {
|
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": {
|
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-brands-svg-icons": {
|
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "(CC-BY-4.0 AND MIT)",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-regular-svg-icons": {
|
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "(CC-BY-4.0 AND MIT)",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-solid-svg-icons": {
|
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "(CC-BY-4.0 AND MIT)",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -2288,6 +2363,485 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/classnames": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/glob": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^5.0.1",
|
||||||
|
"once": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/react-responsive": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==",
|
||||||
|
"dependencies": {
|
||||||
|
"hyphenate-style-name": "^1.0.0",
|
||||||
|
"matchmediaquery": "^0.3.0",
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"shallow-equal": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-footer/node_modules/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-hJCWfdEed8h7aRKmo5lCMyemtMR272q/g1WOetMy8C8ZzeNvf8t94WPFCK4OlbHWTQBd5UiaqGWQWmaGXm0jVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@edx/paragon": "21.1.10",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "6.4.2",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"axios-mock-adapter": "1.21.5",
|
||||||
|
"babel-polyfill": "6.26.0",
|
||||||
|
"react-responsive": "8.2.0",
|
||||||
|
"react-transition-group": "4.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react": "^16.9.0 || ^17.0.0",
|
||||||
|
"react-dom": "^16.9.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@edx/paragon": {
|
||||||
|
"version": "21.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-21.1.10.tgz",
|
||||||
|
"integrity": "sha512-5U8tUaL20gDiKfEDr/tuRXrl7fJsN+KgAIn5bWkTtS5Us7r+H+m3LkD58HY7Ntwj8bCrSEtW7YuK3PMabXcMRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||||
|
"@popperjs/core": "^2.11.4",
|
||||||
|
"bootstrap": "^4.6.2",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"child_process": "^1.0.2",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"email-prop-type": "^3.0.0",
|
||||||
|
"file-selector": "^0.6.0",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"glob": "^8.0.3",
|
||||||
|
"inquirer": "^8.2.5",
|
||||||
|
"lodash.uniqby": "^4.7.0",
|
||||||
|
"mailto-link": "^2.0.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-bootstrap": "^1.6.5",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
|
"react-dropzone": "^14.2.1",
|
||||||
|
"react-focus-on": "^3.5.4",
|
||||||
|
"react-loading-skeleton": "^3.1.0",
|
||||||
|
"react-popper": "^2.2.5",
|
||||||
|
"react-proptype-conditional-require": "^1.0.4",
|
||||||
|
"react-responsive": "^8.2.0",
|
||||||
|
"react-table": "^7.7.0",
|
||||||
|
"react-transition-group": "^4.4.2",
|
||||||
|
"tabbable": "^5.3.3",
|
||||||
|
"uncontrollable": "^7.2.1",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"paragon": "bin/paragon-scripts.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.6 || ^17.0.0",
|
||||||
|
"react-dom": "^16.8.6 || ^17.0.0",
|
||||||
|
"react-intl": "^5.25.1 || ^6.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@edx/paragon/node_modules/@fortawesome/react-fontawesome": {
|
||||||
|
"version": "0.1.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
|
||||||
|
"integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||||
|
"react": ">=16.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@edx/paragon/node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/react-fontawesome": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||||
|
"react": ">=16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/react-fontawesome/node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/axios-mock-adapter": {
|
||||||
|
"version": "1.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.5.tgz",
|
||||||
|
"integrity": "sha512-5NI1V/VK+8+JeTF8niqOowuysA4b8mGzdlMN/QnTnoXbYh4HZSNiopsDclN2g/m85+G++IrEtUdZaQ3GnaMsSA==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"axios": ">= 0.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/classnames": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/glob": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^5.0.1",
|
||||||
|
"once": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/react-responsive": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==",
|
||||||
|
"dependencies": {
|
||||||
|
"hyphenate-style-name": "^1.0.0",
|
||||||
|
"matchmediaquery": "^0.3.0",
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"shallow-equal": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@edx/frontend-component-header/node_modules/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@edx/frontend-enterprise-hotjar": {
|
"node_modules/@edx/frontend-enterprise-hotjar": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
@@ -2298,9 +2852,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-lib-content-components": {
|
"node_modules/@edx/frontend-lib-content-components": {
|
||||||
"version": "1.169.3",
|
"version": "1.174.0",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-content-components/-/frontend-lib-content-components-1.169.3.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-content-components/-/frontend-lib-content-components-1.174.0.tgz",
|
||||||
"integrity": "sha512-CgqMs9vEkx9rw6DBlLThKB/omJzMD+mntZGRevF547G/Gl8dt/KSquNZrOiNph1PImXTD1eb2DtSyN7VbYNBaA==",
|
"integrity": "sha512-Vwf9XHEYZ4yA9J40AsIfCgn6rF/SPBual2P3/FoniRy2MazfXALA7ikAAWaoGdn0RGfgY51TaTcum4A4ImE0uw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-html": "^6.0.0",
|
"@codemirror/lang-html": "^6.0.0",
|
||||||
"@codemirror/lang-xml": "^6.0.0",
|
"@codemirror/lang-xml": "^6.0.0",
|
||||||
@@ -2336,7 +2890,7 @@
|
|||||||
"xmlchecker": "^0.1.0"
|
"xmlchecker": "^0.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@edx/frontend-platform": "^4.0.0",
|
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
|
||||||
"@edx/paragon": "^20.27.0",
|
"@edx/paragon": "^20.27.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^16.14.0 || ^17.0.0",
|
"react": "^16.14.0 || ^17.0.0",
|
||||||
@@ -2536,9 +3090,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/paragon": {
|
"node_modules/@edx/paragon": {
|
||||||
"version": "20.45.4",
|
"version": "20.46.2",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.45.4.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.2.tgz",
|
||||||
"integrity": "sha512-ifkkBR4PRGlsFdMwuyYznUMrifyaO9Yy0PyTsP2iD99Pn5ZZMqYOmtvMm8Ek087ABbc/7MIwzxWXMhTvQpNVNw==",
|
"integrity": "sha512-px+KS/BV1CbiMKgfVgUofyjJi4CHUCUOLRukJbT66VPPqWP4Xon5Rns6uohoratPXMg2kNN46v2L8wIwqKQ4Lw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||||
@@ -5513,7 +6067,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.21.3"
|
"type-fest": "^0.21.3"
|
||||||
@@ -5538,7 +6091,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -5915,7 +6467,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "0.21.4",
|
"version": "0.21.4",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.14.0"
|
"follow-redirects": "^1.14.0"
|
||||||
@@ -6323,7 +6874,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -6363,7 +6913,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
@@ -6373,7 +6922,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/bl/node_modules/readable-stream": {
|
"node_modules/bl/node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
@@ -6548,7 +7096,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -6786,6 +7333,11 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chardet": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||||
|
},
|
||||||
"node_modules/cheerio": {
|
"node_modules/cheerio": {
|
||||||
"version": "1.0.0-rc.12",
|
"version": "1.0.0-rc.12",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -6822,6 +7374,11 @@
|
|||||||
"url": "https://github.com/sponsors/fb55"
|
"url": "https://github.com/sponsors/fb55"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/child_process": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g=="
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7009,6 +7566,36 @@
|
|||||||
"webpack": "*"
|
"webpack": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-cursor": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
|
||||||
|
"dependencies": {
|
||||||
|
"restore-cursor": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-spinners": {
|
||||||
|
"version": "2.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz",
|
||||||
|
"integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-width": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "7.0.4",
|
"version": "7.0.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7019,6 +7606,14 @@
|
|||||||
"wrap-ansi": "^7.0.0"
|
"wrap-ansi": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clone-deep": {
|
"node_modules/clone-deep": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7907,6 +8502,17 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/defaults": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"clone": "^1.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/define-lazy-prop": {
|
"node_modules/define-lazy-prop": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8586,7 +9192,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
@@ -9738,6 +10343,30 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/external-editor": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
|
||||||
|
"dependencies": {
|
||||||
|
"chardet": "^0.7.0",
|
||||||
|
"iconv-lite": "^0.4.24",
|
||||||
|
"tmp": "^0.0.33"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/external-editor/node_modules/iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extglob": {
|
"node_modules/extglob": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9788,7 +10417,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-defer": {
|
"node_modules/fast-defer": {
|
||||||
@@ -9855,6 +10483,20 @@
|
|||||||
"bser": "2.1.1"
|
"bser": "2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/figures": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9885,6 +10527,11 @@
|
|||||||
"webpack": "^4.0.0 || ^5.0.0"
|
"webpack": "^4.0.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"node_modules/file-selector": {
|
"node_modules/file-selector": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -11251,7 +11898,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -11458,6 +12104,108 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/inquirer": {
|
||||||
|
"version": "8.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz",
|
||||||
|
"integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-escapes": "^4.2.1",
|
||||||
|
"chalk": "^4.1.1",
|
||||||
|
"cli-cursor": "^3.1.0",
|
||||||
|
"cli-width": "^3.0.0",
|
||||||
|
"external-editor": "^3.0.3",
|
||||||
|
"figures": "^3.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mute-stream": "0.0.8",
|
||||||
|
"ora": "^5.4.1",
|
||||||
|
"run-async": "^2.4.0",
|
||||||
|
"rxjs": "^7.5.5",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"through": "^2.3.6",
|
||||||
|
"wrap-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -11591,7 +12339,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/is-buffer": {
|
"node_modules/is-buffer": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -11725,7 +12472,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -11754,6 +12500,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-interactive": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-invalid-path": {
|
"node_modules/is-invalid-path": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -11980,6 +12734,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-unicode-supported": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-valid-path": {
|
"node_modules/is-valid-path": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -14674,6 +15439,85 @@
|
|||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/log-symbols": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"is-unicode-supported": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -15005,7 +15849,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/mimic-fn": {
|
"node_modules/mimic-fn": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -15220,6 +16063,11 @@
|
|||||||
"multicast-dns": "cli.js"
|
"multicast-dns": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mute-stream": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
||||||
|
},
|
||||||
"node_modules/mux.js": {
|
"node_modules/mux.js": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -15884,7 +16732,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/onetime": {
|
"node_modules/onetime": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mimic-fn": "^2.1.0"
|
"mimic-fn": "^2.1.0"
|
||||||
@@ -15944,6 +16791,100 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ora": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.1.0",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"cli-cursor": "^3.1.0",
|
||||||
|
"cli-spinners": "^2.5.0",
|
||||||
|
"is-interactive": "^1.0.0",
|
||||||
|
"is-unicode-supported": "^0.1.0",
|
||||||
|
"log-symbols": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wcwidth": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ora/node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ora/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ora/node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ora/node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"node_modules/ora/node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ora/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/os-tmpdir": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-each-series": {
|
"node_modules/p-each-series": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -17902,15 +18843,6 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-ranger": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-ranger/-/react-ranger-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-d8ezhyX3v/KlN8SkyoE5e8Dybsdmn94eUAqBDsAPrVhZde8sVt6pLJw4fC784KiMmS4LyAjvtjGxhAEqjjGYgw==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.6.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -18671,6 +19603,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/restore-cursor": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
|
||||||
|
"dependencies": {
|
||||||
|
"onetime": "^5.1.0",
|
||||||
|
"signal-exit": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ret": {
|
"node_modules/ret": {
|
||||||
"version": "0.1.15",
|
"version": "0.1.15",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -18738,6 +19682,14 @@
|
|||||||
"rtlcss": "bin/rtlcss.js"
|
"rtlcss": "bin/rtlcss.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/run-async": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-node": {
|
"node_modules/run-node": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -18778,6 +19730,14 @@
|
|||||||
"individual": "^2.0.0"
|
"individual": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rxjs": {
|
||||||
|
"version": "7.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||||
|
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -18811,7 +19771,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sane": {
|
"node_modules/sane": {
|
||||||
@@ -19420,7 +20379,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/simple-concat": {
|
"node_modules/simple-concat": {
|
||||||
@@ -20104,7 +21062,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
@@ -20117,7 +21074,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/string-width/node_modules/emoji-regex": {
|
"node_modules/string-width/node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/string.prototype.matchall": {
|
"node_modules/string.prototype.matchall": {
|
||||||
@@ -20182,7 +21138,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
@@ -20930,6 +21885,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/through": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
||||||
|
},
|
||||||
"node_modules/thunky": {
|
"node_modules/thunky": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -20947,6 +21907,17 @@
|
|||||||
"version": "5.10.5",
|
"version": "5.10.5",
|
||||||
"license": "LGPL-2.1"
|
"license": "LGPL-2.1"
|
||||||
},
|
},
|
||||||
|
"node_modules/tmp": {
|
||||||
|
"version": "0.0.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||||
|
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"os-tmpdir": "~1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -21140,7 +22111,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"dev": true,
|
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -21680,6 +22650,14 @@
|
|||||||
"minimalistic-assert": "^1.0.0"
|
"minimalistic-assert": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wcwidth": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
|
||||||
|
"dependencies": {
|
||||||
|
"defaults": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -35,9 +35,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||||
"@edx/frontend-component-footer": "12.0.0",
|
"@edx/frontend-component-footer": "^12.3.0",
|
||||||
|
"@edx/frontend-component-header": "^4.7.0",
|
||||||
"@edx/frontend-enterprise-hotjar": "^1.2.1",
|
"@edx/frontend-enterprise-hotjar": "^1.2.1",
|
||||||
"@edx/frontend-lib-content-components": "^1.169.3",
|
"@edx/frontend-lib-content-components": "^1.174.0",
|
||||||
"@edx/frontend-platform": "4.2.0",
|
"@edx/frontend-platform": "4.2.0",
|
||||||
"@edx/paragon": "^20.45.4",
|
"@edx/paragon": "^20.45.4",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"core-js": "3.8.1",
|
"core-js": "3.8.1",
|
||||||
"email-validator": "2.0.4",
|
"email-validator": "2.0.4",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"formik": "2.2.6",
|
"formik": "2.2.6",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
@@ -58,7 +60,6 @@
|
|||||||
"react-datepicker": "^4.13.0",
|
"react-datepicker": "^4.13.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-ranger": "^2.1.0",
|
|
||||||
"react-redux": "7.1.3",
|
"react-redux": "7.1.3",
|
||||||
"react-responsive": "8.1.0",
|
"react-responsive": "8.1.0",
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
"react-transition-group": "4.4.1",
|
"react-transition-group": "4.4.1",
|
||||||
"redux": "4.0.5",
|
"redux": "4.0.5",
|
||||||
"regenerator-runtime": "0.13.7",
|
"regenerator-runtime": "0.13.7",
|
||||||
|
"universal-cookie": "^4.0.4",
|
||||||
"uuid": "^3.4.0",
|
"uuid": "^3.4.0",
|
||||||
"yup": "0.31.1"
|
"yup": "0.31.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<title>Course Authoring | <%= process.env.SITE_NAME %></title>
|
<title>Course Authoring | <%= process.env.SITE_NAME %></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="<%= process.env.FAVICON_URL %>" type="image/x-icon" />
|
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"rebaseStalePrs": true,
|
"rebaseStalePrs": true,
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchPackagePatterns": ["@edx"],
|
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
"automerge": true
|
"automerge": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import {
|
import {
|
||||||
useLocation,
|
useLocation,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { Footer } from '@edx/frontend-lib-content-components';
|
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||||
import Header from './studio-header/Header';
|
import Header from './header';
|
||||||
import { fetchCourseDetail } from './data/thunks';
|
import { fetchCourseDetail } from './data/thunks';
|
||||||
import { useModel } from './generic/model-store';
|
import { useModel } from './generic/model-store';
|
||||||
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
||||||
@@ -37,21 +37,6 @@ AppHeader.defaultProps = {
|
|||||||
courseOrg: null,
|
courseOrg: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppFooter = () => (
|
|
||||||
<div className="mt-6">
|
|
||||||
<Footer
|
|
||||||
marketingBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
|
||||||
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
|
||||||
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
|
||||||
supportEmail={process.env.SUPPORT_EMAIL}
|
|
||||||
platformName={process.env.SITE_NAME}
|
|
||||||
lmsBaseUrl={process.env.LMS_BASE_URL}
|
|
||||||
studioBaseUrl={process.env.STUDIO_BASE_URL}
|
|
||||||
showAccessibilityPage={process.env.ENABLE_ACCESSIBILITY_PAGE === 'true'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const CourseAuthoringPage = ({ courseId, children }) => {
|
const CourseAuthoringPage = ({ courseId, children }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -91,7 +76,7 @@ const CourseAuthoringPage = ({ courseId, children }) => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
{!inProgress && showHeader && <AppFooter />}
|
{!inProgress && showHeader && <StudioFooter />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import ScheduleAndDetails from './schedule-and-details';
|
|||||||
import { GradingSettings } from './grading-settings';
|
import { GradingSettings } from './grading-settings';
|
||||||
import CourseTeam from './course-team/CourseTeam';
|
import CourseTeam from './course-team/CourseTeam';
|
||||||
import { CourseUpdates } from './course-updates';
|
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:
|
* As of this writing, these routes are mounted at a path prefixed with the following:
|
||||||
@@ -99,16 +101,10 @@ const CourseAuthoringRoutes = ({ courseId }) => {
|
|||||||
<AdvancedSettings courseId={courseId} />
|
<AdvancedSettings courseId={courseId} />
|
||||||
</PageRoute>
|
</PageRoute>
|
||||||
<PageRoute path={`${path}/import`}>
|
<PageRoute path={`${path}/import`}>
|
||||||
{process.env.ENABLE_NEW_IMPORT_PAGE === 'true'
|
<CourseImportPage courseId={courseId} />
|
||||||
&& (
|
|
||||||
<Placeholder />
|
|
||||||
)}
|
|
||||||
</PageRoute>
|
</PageRoute>
|
||||||
<PageRoute path={`${path}/export`}>
|
<PageRoute path={`${path}/export`}>
|
||||||
{process.env.ENABLE_NEW_EXPORT_PAGE === 'true'
|
<CourseExportPage courseId={courseId} />
|
||||||
&& (
|
|
||||||
<Placeholder />
|
|
||||||
)}
|
|
||||||
</PageRoute>
|
</PageRoute>
|
||||||
</Switch>
|
</Switch>
|
||||||
</CourseAuthoringPage>
|
</CourseAuthoringPage>
|
||||||
|
|||||||
@@ -65,9 +65,7 @@ describe('<CourseAuthoringRoutes>', () => {
|
|||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: This test needs to be corrected.
|
fit('renders the PagesAndResources component when the pages and resources route is active', () => {
|
||||||
// The problem arose after moving new commits (https://github.com/raccoongang/frontend-app-course-authoring/pull/25)
|
|
||||||
it.skip('renders the PagesAndResources component when the pages and resources route is active', () => {
|
|
||||||
render(
|
render(
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<MemoryRouter initialEntries={[`/course/${courseId}/pages-and-resources`]}>
|
<MemoryRouter initialEntries={[`/course/${courseId}/pages-and-resources`]}>
|
||||||
@@ -76,7 +74,7 @@ describe('<CourseAuthoringRoutes>', () => {
|
|||||||
</AppProvider>,
|
</AppProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByText(pagesAndResourcesMockText)).toBeInTheDocument();
|
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
|
||||||
expect(screen.queryByText(proctoredExamSeetingsMockText)).not.toBeInTheDocument();
|
expect(screen.queryByText(proctoredExamSeetingsMockText)).not.toBeInTheDocument();
|
||||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -85,9 +83,7 @@ describe('<CourseAuthoringRoutes>', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: This test needs to be corrected.
|
it('renders the ProctoredExamSettings component when the proctored exam settings route is active', () => {
|
||||||
// The problem arose after moving new commits (https://github.com/raccoongang/frontend-app-course-authoring/pull/25)
|
|
||||||
it.skip('renders the ProctoredExamSettings component when the proctored exam settings route is active', () => {
|
|
||||||
render(
|
render(
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<MemoryRouter initialEntries={[`/course/${courseId}/proctored-exam-settings`]}>
|
<MemoryRouter initialEntries={[`/course/${courseId}/proctored-exam-settings`]}>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
|
|||||||
import Placeholder from '@edx/frontend-lib-content-components';
|
import Placeholder from '@edx/frontend-lib-content-components';
|
||||||
|
|
||||||
import AlertProctoringError from '../generic/AlertProctoringError';
|
import AlertProctoringError from '../generic/AlertProctoringError';
|
||||||
|
import { useModel } from '../generic/model-store';
|
||||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||||
import { parseArrayOrObjectValues } from '../utils';
|
import { parseArrayOrObjectValues } from '../utils';
|
||||||
import { RequestStatus } from '../data/constants';
|
import { RequestStatus } from '../data/constants';
|
||||||
@@ -23,6 +24,7 @@ import SettingsSidebar from './settings-sidebar/SettingsSidebar';
|
|||||||
import validateAdvancedSettingsData from './utils';
|
import validateAdvancedSettingsData from './utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ModalError from './modal-error/ModalError';
|
import ModalError from './modal-error/ModalError';
|
||||||
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
|
||||||
const AdvancedSettings = ({ intl, courseId }) => {
|
const AdvancedSettings = ({ intl, courseId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -36,6 +38,9 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
|||||||
const [isEditableState, setIsEditableState] = useState(false);
|
const [isEditableState, setIsEditableState] = useState(false);
|
||||||
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
|
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
|
||||||
|
|
||||||
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchCourseAppSettings(courseId));
|
dispatch(fetchCourseAppSettings(courseId));
|
||||||
dispatch(fetchProctoringExamErrors(courseId));
|
dispatch(fetchProctoringExamErrors(courseId));
|
||||||
|
|||||||
@@ -40,16 +40,11 @@
|
|||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
width: $setting-form-control-width;
|
flex-grow: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.pgn__card-section {
|
|
||||||
max-width: $setting-form-control-width;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pgn__card-header {
|
.pgn__card-header {
|
||||||
padding: 0 0 0 1.5rem;
|
padding: 0 0 0 1.5rem;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pgn__card-status {
|
.pgn__card-status {
|
||||||
@@ -59,8 +54,6 @@
|
|||||||
.pgn__card-header-content {
|
.pgn__card-header-content {
|
||||||
margin-top: 1.438rem;
|
margin-top: 1.438rem;
|
||||||
margin-bottom: 1.438rem;
|
margin-bottom: 1.438rem;
|
||||||
display: flex;
|
|
||||||
flex-direction: revert;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
$text-color-base: $gray-700;
|
$text-color-base: $gray-700;
|
||||||
$setting-form-control-width: 34.375rem;
|
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ const SettingCard = ({
|
|||||||
return (
|
return (
|
||||||
<li className="field-group course-advanced-policy-list-item">
|
<li className="field-group course-advanced-policy-list-item">
|
||||||
<Card className="flex-column setting-card">
|
<Card className="flex-column setting-card">
|
||||||
<Card.Body className="d-flex">
|
<Card.Body className="d-flex row m-0 align-items-center">
|
||||||
<Card.Header
|
<Card.Header
|
||||||
|
className="col-6"
|
||||||
title={(
|
title={(
|
||||||
<ActionRow>
|
<ActionRow>
|
||||||
{capitalize(displayName)}
|
{capitalize(displayName)}
|
||||||
@@ -70,7 +71,7 @@ const SettingCard = ({
|
|||||||
iconAs={Icon}
|
iconAs={Icon}
|
||||||
alt={intl.formatMessage(messages.helpButtonText)}
|
alt={intl.formatMessage(messages.helpButtonText)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className=" ml-1 mr-2"
|
className="flex-shrink-0 ml-1 mr-2"
|
||||||
/>
|
/>
|
||||||
<ModalPopup
|
<ModalPopup
|
||||||
hasArrow
|
hasArrow
|
||||||
@@ -86,10 +87,11 @@ const SettingCard = ({
|
|||||||
dangerouslySetInnerHTML={{ __html: help }}
|
dangerouslySetInnerHTML={{ __html: help }}
|
||||||
/>
|
/>
|
||||||
</ModalPopup>
|
</ModalPopup>
|
||||||
|
<ActionRow.Spacer />
|
||||||
</ActionRow>
|
</ActionRow>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Section>
|
<Card.Section className="col-6 flex-grow-1">
|
||||||
<Form.Group className="m-0">
|
<Form.Group className="m-0">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as={TextareaAutosize}
|
as={TextareaAutosize}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import HelpSidebar from '../../generic/help-sidebar';
|
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const SettingsSidebar = ({ intl, courseId, proctoredExamSettingsUrl }) => (
|
const SettingsSidebar = ({ intl, courseId, proctoredExamSettingsUrl }) => (
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ export const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss\\Z';
|
|||||||
export const COMMA_SEPARATED_DATE_FORMAT = 'MMMM D, YYYY';
|
export const COMMA_SEPARATED_DATE_FORMAT = 'MMMM D, YYYY';
|
||||||
export const DEFAULT_EMPTY_WYSIWYG_VALUE = '<p> </p>';
|
export const DEFAULT_EMPTY_WYSIWYG_VALUE = '<p> </p>';
|
||||||
export const STATEFUL_BUTTON_STATES = {
|
export const STATEFUL_BUTTON_STATES = {
|
||||||
pending: 'pending',
|
|
||||||
default: 'default',
|
default: 'default',
|
||||||
|
pending: 'pending',
|
||||||
|
error: 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const USER_ROLES = {
|
export const USER_ROLES = {
|
||||||
@@ -23,3 +24,13 @@ export const NOTIFICATION_MESSAGES = {
|
|||||||
duplicating: 'Duplicating',
|
duplicating: 'Duplicating',
|
||||||
deleting: 'Deleting',
|
deleting: 'Deleting',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_TIME_STAMP = '00:00';
|
||||||
|
|
||||||
|
export const COURSE_CREATOR_STATES = {
|
||||||
|
unrequested: 'unrequested',
|
||||||
|
pending: 'pending',
|
||||||
|
granted: 'granted',
|
||||||
|
denied: 'denied',
|
||||||
|
disallowedForThisSite: 'disallowed_for_this_site',
|
||||||
|
};
|
||||||
|
|||||||
93
src/course-rerun/CourseRerun.test.jsx
Normal file
93
src/course-rerun/CourseRerun.test.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { history, initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
import {
|
||||||
|
act, fireEvent, render, waitFor,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
|
import initializeStore from '../store';
|
||||||
|
import { studioHomeMock } from '../studio-home/__mocks__';
|
||||||
|
import { getStudioHomeApiUrl } from '../studio-home/data/api';
|
||||||
|
import { RequestStatus } from '../data/constants';
|
||||||
|
import messages from './messages';
|
||||||
|
import CourseRerun from '.';
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
const mockPathname = '/foo-bar';
|
||||||
|
|
||||||
|
jest.mock('react-redux', () => ({
|
||||||
|
...jest.requireActual('react-redux'),
|
||||||
|
useSelector: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: () => ({
|
||||||
|
pathname: mockPathname,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const RootWrapper = () => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<CourseRerun intl={injectIntl} />
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<CourseRerun />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store = initializeStore();
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||||
|
useSelector.mockReturnValue(studioHomeMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render successfully', () => {
|
||||||
|
const { getByText, getAllByRole } = render(<RootWrapper />);
|
||||||
|
expect(getByText(messages.rerunTitle.defaultMessage));
|
||||||
|
expect(getAllByRole('button', { name: messages.cancelButton.defaultMessage }).length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to /home on cancel button click', () => {
|
||||||
|
const { getAllByRole } = render(<RootWrapper />);
|
||||||
|
const cancelButton = getAllByRole('button', { name: messages.cancelButton.defaultMessage })[0];
|
||||||
|
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
waitFor(() => {
|
||||||
|
expect(history.location.pathname).toBe('/home');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the spinner before the query is complete', async () => {
|
||||||
|
useSelector.mockReturnValue({ organizationLoadingStatus: RequestStatus.IN_PROGRESS });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const { getByRole } = render(<RootWrapper />);
|
||||||
|
const spinner = getByRole('status');
|
||||||
|
expect(spinner.textContent).toEqual('Loading...');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show footer', () => {
|
||||||
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
|
||||||
|
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx
Normal file
58
src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import { studioHomeMock } from '../../studio-home/__mocks__';
|
||||||
|
import initializeStore from '../../store';
|
||||||
|
import CourseRerunForm from '.';
|
||||||
|
|
||||||
|
jest.mock('react-redux', () => ({
|
||||||
|
...jest.requireActual('react-redux'),
|
||||||
|
useSelector: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let store;
|
||||||
|
|
||||||
|
const onClickCancelMock = jest.fn();
|
||||||
|
|
||||||
|
const RootWrapper = (props) => (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<CourseRerunForm {...props} />
|
||||||
|
</AppProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
initialFormValues: {
|
||||||
|
displayName: '',
|
||||||
|
org: '',
|
||||||
|
number: '',
|
||||||
|
run: '',
|
||||||
|
},
|
||||||
|
onClickCancel: onClickCancelMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<CourseRerunForm />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store = initializeStore();
|
||||||
|
useSelector.mockReturnValue(studioHomeMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders description successfully', () => {
|
||||||
|
const { getByText } = render(<RootWrapper {...props} />);
|
||||||
|
expect(getByText('Provide identifying information for this re-run of the course. The original course is not affected in any way by a re-run', { exact: false })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
36
src/course-rerun/course-rerun-form/index.jsx
Normal file
36
src/course-rerun/course-rerun-form/index.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { CreateOrRerunCourseForm } from '../../generic/create-or-rerun-course';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const CourseRerunForm = ({ initialFormValues, onClickCancel }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<div className="mb-4.5">
|
||||||
|
<div className="my-2.5">{intl.formatMessage(messages.rerunCourseDescription, {
|
||||||
|
strong: (
|
||||||
|
<strong>{intl.formatMessage(messages.rerunCourseDescriptionStrong)}</strong>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<CreateOrRerunCourseForm
|
||||||
|
initialValues={initialFormValues}
|
||||||
|
onClickCancel={onClickCancel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CourseRerunForm.propTypes = {
|
||||||
|
initialFormValues: PropTypes.shape({
|
||||||
|
displayName: PropTypes.string.isRequired,
|
||||||
|
org: PropTypes.string.isRequired,
|
||||||
|
number: PropTypes.string.isRequired,
|
||||||
|
run: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
onClickCancel: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseRerunForm;
|
||||||
14
src/course-rerun/course-rerun-form/messages.js
Normal file
14
src/course-rerun/course-rerun-form/messages.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
rerunCourseDescription: {
|
||||||
|
id: 'course-authoring.course-rerun.form.description',
|
||||||
|
defaultMessage: 'Provide identifying information for this re-run of the course. The original course is not affected in any way by a re-run. {strong}',
|
||||||
|
},
|
||||||
|
rerunCourseDescriptionStrong: {
|
||||||
|
id: 'course-authoring.course-rerun.form.description.strong',
|
||||||
|
defaultMessage: 'Note: Together, the organization, course number, and course run must uniquely identify this new course instance.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import initializeStore from '../../store';
|
||||||
|
import CourseRerunSideBar from '.';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
let store;
|
||||||
|
const mockPathname = '/foo-bar';
|
||||||
|
const courseId = '123';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: () => ({
|
||||||
|
pathname: mockPathname,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const renderComponent = (props) => render(
|
||||||
|
<AppProvider store={store} messages={{}}>
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<CourseRerunSideBar courseId={courseId} {...props} />
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<CourseRerunSideBar />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store = initializeStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('render CourseRerunSideBar successfully', () => {
|
||||||
|
const { getByText } = renderComponent();
|
||||||
|
|
||||||
|
expect(getByText(messages.sectionTitle1.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.sectionDescription1.defaultMessage, { exact: false })).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.sectionTitle2.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.sectionDescription2.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.sectionTitle3.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.sectionDescription3.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.sectionLink4.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
83
src/course-rerun/course-rerun-sidebar/index.jsx
Normal file
83
src/course-rerun/course-rerun-sidebar/index.jsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { Hyperlink } from '@edx/paragon';
|
||||||
|
import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { useHelpUrls } from '../../help-urls/hooks';
|
||||||
|
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const CourseRerunSideBar = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { default: learnMoreUrl } = useHelpUrls(['default']);
|
||||||
|
const defaultCourseDate = new Date(Date.UTC(2030, 0, 1, 0, 0));
|
||||||
|
const localizedCourseDate = (
|
||||||
|
<FormattedDate
|
||||||
|
value={defaultCourseDate}
|
||||||
|
year="numeric"
|
||||||
|
month="long"
|
||||||
|
day="2-digit"
|
||||||
|
hour="numeric"
|
||||||
|
minute="numeric"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sidebarMessages = [
|
||||||
|
{
|
||||||
|
title: intl.formatMessage(messages.sectionTitle1),
|
||||||
|
description: `${intl.formatMessage(messages.sectionDescription1)}`,
|
||||||
|
date: localizedCourseDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage(messages.sectionTitle2),
|
||||||
|
description: intl.formatMessage(messages.sectionDescription2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage(messages.sectionTitle3),
|
||||||
|
description: intl.formatMessage(messages.sectionDescription3),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: {
|
||||||
|
text: intl.formatMessage(messages.sectionLink4),
|
||||||
|
href: learnMoreUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HelpSidebar
|
||||||
|
intl={intl}
|
||||||
|
showOtherSettings={false}
|
||||||
|
className="mt-3"
|
||||||
|
>
|
||||||
|
{sidebarMessages.map(({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
link,
|
||||||
|
date,
|
||||||
|
}, index) => {
|
||||||
|
const isLastSection = index === sidebarMessages.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={uuid()}>
|
||||||
|
<h4 className="help-sidebar-about-title">{title}</h4>
|
||||||
|
<p className="help-sidebar-about-descriptions">{description} {date}</p>
|
||||||
|
{!!link && (
|
||||||
|
<Hyperlink
|
||||||
|
className="small"
|
||||||
|
destination={link.href || ''}
|
||||||
|
target="_blank"
|
||||||
|
showLaunchIcon={false}
|
||||||
|
>
|
||||||
|
{link.text}
|
||||||
|
</Hyperlink>
|
||||||
|
)}
|
||||||
|
{!isLastSection && <hr className="my-3.5" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</HelpSidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseRerunSideBar;
|
||||||
34
src/course-rerun/course-rerun-sidebar/messages.js
Normal file
34
src/course-rerun/course-rerun-sidebar/messages.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
sectionTitle1: {
|
||||||
|
id: 'course-authoring.course-rerun.sidebar.section-1.title',
|
||||||
|
defaultMessage: 'When will my course re-run start?',
|
||||||
|
},
|
||||||
|
sectionDescription1: {
|
||||||
|
id: 'course-authoring.course-rerun.sidebar.section-1.description',
|
||||||
|
defaultMessage: 'The new course is set to start on',
|
||||||
|
},
|
||||||
|
sectionTitle2: {
|
||||||
|
id: 'course-authoring.course-rerun.sidebar.section-2.title',
|
||||||
|
defaultMessage: 'What transfers from the original course?',
|
||||||
|
},
|
||||||
|
sectionDescription2: {
|
||||||
|
id: 'course-authoring.course-rerun.sidebar.section-2.description',
|
||||||
|
defaultMessage: 'The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.',
|
||||||
|
},
|
||||||
|
sectionTitle3: {
|
||||||
|
id: 'course-authoring.course-rerun.sidebar.section-3.title',
|
||||||
|
defaultMessage: 'What does not transfer from the original course?',
|
||||||
|
},
|
||||||
|
sectionDescription3: {
|
||||||
|
id: 'course-authoring.course-rerun.sidebar.section-3.description',
|
||||||
|
defaultMessage: 'You are the only member of the new course\'s staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.',
|
||||||
|
},
|
||||||
|
sectionLink4: {
|
||||||
|
id: 'course-authoring.course-rerun.sidebar.section-4.link',
|
||||||
|
defaultMessage: 'Learn more about course re-runs',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
67
src/course-rerun/hooks.jsx
Normal file
67
src/course-rerun/hooks.jsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { history } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { RequestStatus } from '../data/constants';
|
||||||
|
import { updateSavingStatus } from '../generic/data/slice';
|
||||||
|
import {
|
||||||
|
getSavingStatus,
|
||||||
|
getRedirectUrlObj,
|
||||||
|
getCourseRerunData,
|
||||||
|
getCourseData,
|
||||||
|
} from '../generic/data/selectors';
|
||||||
|
import { fetchCourseRerunQuery, fetchOrganizationsQuery } from '../generic/data/thunks';
|
||||||
|
import { fetchStudioHomeData } from '../studio-home/data/thunks';
|
||||||
|
|
||||||
|
const useCourseRerun = (courseId) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const savingStatus = useSelector(getSavingStatus);
|
||||||
|
const courseData = useSelector(getCourseData);
|
||||||
|
const courseRerunData = useSelector(getCourseRerunData);
|
||||||
|
const redirectUrlObj = useSelector(getRedirectUrlObj);
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayName = '',
|
||||||
|
org = '',
|
||||||
|
run = '',
|
||||||
|
number = '',
|
||||||
|
} = courseRerunData;
|
||||||
|
const originalCourseData = `${org} ${number} ${run}`;
|
||||||
|
const initialFormValues = {
|
||||||
|
displayName,
|
||||||
|
org,
|
||||||
|
number,
|
||||||
|
run: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchStudioHomeData());
|
||||||
|
dispatch(fetchCourseRerunQuery(courseId));
|
||||||
|
dispatch(fetchOrganizationsQuery());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
||||||
|
dispatch(updateSavingStatus({ status: '' }));
|
||||||
|
const { url } = redirectUrlObj;
|
||||||
|
if (url) {
|
||||||
|
history.push('/home');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [savingStatus]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
intl,
|
||||||
|
courseData,
|
||||||
|
displayName,
|
||||||
|
savingStatus,
|
||||||
|
initialFormValues,
|
||||||
|
originalCourseData,
|
||||||
|
dispatch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export { useCourseRerun };
|
||||||
99
src/course-rerun/index.jsx
Normal file
99
src/course-rerun/index.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Layout,
|
||||||
|
Stack,
|
||||||
|
ActionRow,
|
||||||
|
Button,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { history } from '@edx/frontend-platform';
|
||||||
|
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||||
|
|
||||||
|
import Header from '../header';
|
||||||
|
import Loading from '../generic/Loading';
|
||||||
|
import { getLoadingStatuses } from '../generic/data/selectors';
|
||||||
|
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||||
|
import { RequestStatus } from '../data/constants';
|
||||||
|
import CourseRerunForm from './course-rerun-form';
|
||||||
|
import CourseRerunSideBar from './course-rerun-sidebar';
|
||||||
|
import messages from './messages';
|
||||||
|
import { useCourseRerun } from './hooks';
|
||||||
|
|
||||||
|
const CourseRerun = ({ courseId }) => {
|
||||||
|
const {
|
||||||
|
intl,
|
||||||
|
displayName,
|
||||||
|
savingStatus,
|
||||||
|
initialFormValues,
|
||||||
|
originalCourseData,
|
||||||
|
} = useCourseRerun(courseId);
|
||||||
|
const { organizationLoadingStatus } = useSelector(getLoadingStatuses);
|
||||||
|
|
||||||
|
if (organizationLoadingStatus === RequestStatus.IN_PROGRESS) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRerunCourseCancel = () => {
|
||||||
|
history.push('/home');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header isHiddenMainMenu />
|
||||||
|
<Container size="xl" className="small p-4 mt-3">
|
||||||
|
<section className="mb-4">
|
||||||
|
<article>
|
||||||
|
<section>
|
||||||
|
<header className="d-flex">
|
||||||
|
<Stack>
|
||||||
|
<h2>
|
||||||
|
{intl.formatMessage(messages.rerunTitle)} {displayName}
|
||||||
|
</h2>
|
||||||
|
<span className="large">{originalCourseData}</span>
|
||||||
|
</Stack>
|
||||||
|
<ActionRow className="ml-auto">
|
||||||
|
<Button variant="outline-primary" size="sm" onClick={handleRerunCourseCancel}>
|
||||||
|
{intl.formatMessage(messages.cancelButton)}
|
||||||
|
</Button>
|
||||||
|
</ActionRow>
|
||||||
|
</header>
|
||||||
|
<hr />
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<Layout
|
||||||
|
lg={[{ span: 9 }, { span: 3 }]}
|
||||||
|
md={[{ span: 9 }, { span: 3 }]}
|
||||||
|
sm={[{ span: 9 }, { span: 3 }]}
|
||||||
|
xs={[{ span: 9 }, { span: 3 }]}
|
||||||
|
xl={[{ span: 9 }, { span: 3 }]}
|
||||||
|
>
|
||||||
|
<Layout.Element>
|
||||||
|
<CourseRerunForm
|
||||||
|
initialFormValues={initialFormValues}
|
||||||
|
onClickCancel={handleRerunCourseCancel}
|
||||||
|
/>
|
||||||
|
</Layout.Element>
|
||||||
|
<Layout.Element>
|
||||||
|
<CourseRerunSideBar />
|
||||||
|
</Layout.Element>
|
||||||
|
</Layout>
|
||||||
|
</section>
|
||||||
|
</Container>
|
||||||
|
<div className="alert-toast">
|
||||||
|
<InternetConnectionAlert
|
||||||
|
isFailed={savingStatus === RequestStatus.FAILED}
|
||||||
|
isQueryPending={savingStatus === RequestStatus.PENDING}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<StudioFooter />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CourseRerun.propTypes = {
|
||||||
|
courseId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseRerun;
|
||||||
14
src/course-rerun/messages.js
Normal file
14
src/course-rerun/messages.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
rerunTitle: {
|
||||||
|
id: 'course-authoring.course-rerun.title',
|
||||||
|
defaultMessage: 'Create a re-run of',
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
id: 'course-authoring.course-rerun.actions.button.cancel',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { Add as IconAdd } from '@edx/paragon/icons';
|
import { Add as IconAdd } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||||
|
import { useModel } from '../generic/model-store';
|
||||||
import SubHeader from '../generic/sub-header/SubHeader';
|
import SubHeader from '../generic/sub-header/SubHeader';
|
||||||
import { USER_ROLES } from '../constants';
|
import { USER_ROLES } from '../constants';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -18,10 +19,14 @@ import AddTeamMember from './add-team-member/AddTeamMember';
|
|||||||
import CourseTeamMember from './course-team-member/CourseTeamMember';
|
import CourseTeamMember from './course-team-member/CourseTeamMember';
|
||||||
import InfoModal from './info-modal/InfoModal';
|
import InfoModal from './info-modal/InfoModal';
|
||||||
import { useCourseTeam } from './hooks';
|
import { useCourseTeam } from './hooks';
|
||||||
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
|
||||||
const CourseTeam = ({ courseId }) => {
|
const CourseTeam = ({ courseId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
modalType,
|
modalType,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -57,7 +62,7 @@ const CourseTeam = ({ courseId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container size="xl" className="m-4">
|
<Container size="xl" className="px-4">
|
||||||
<section className="course-team-container mb-4">
|
<section className="course-team-container mb-4">
|
||||||
<Layout
|
<Layout
|
||||||
lg={[{ span: 9 }, { span: 3 }]}
|
lg={[{ span: 9 }, { span: 3 }]}
|
||||||
@@ -74,7 +79,7 @@ const CourseTeam = ({ courseId }) => {
|
|||||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||||
headerActions={isAllowActions && (
|
headerActions={isAllowActions && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline-success"
|
variant="primary"
|
||||||
iconBefore={IconAdd}
|
iconBefore={IconAdd}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={openForm}
|
onClick={openForm}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const AddTeamMember = ({ onFormOpen, isButtonDisable }) => {
|
|||||||
<span className="text-gray-500 small">{intl.formatMessage(messages.description)}</span>
|
<span className="text-gray-500 small">{intl.formatMessage(messages.description)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline-success"
|
variant="primary"
|
||||||
iconBefore={IconAdd}
|
iconBefore={IconAdd}
|
||||||
onClick={onFormOpen}
|
onClick={onFormOpen}
|
||||||
disabled={isButtonDisable}
|
disabled={isButtonDisable}
|
||||||
|
|||||||
@@ -4,4 +4,14 @@ export const MODAL_TYPES = {
|
|||||||
warning: 'warning',
|
warning: 'warning',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const BADGE_STATES = {
|
||||||
|
admin: 'primary-700',
|
||||||
|
staff: 'gray-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const USER_ROLES = {
|
||||||
|
admin: 'instructor',
|
||||||
|
staff: 'staff',
|
||||||
|
};
|
||||||
|
|
||||||
export const EXAMPLE_USER_EMAIL = 'username@domain.com';
|
export const EXAMPLE_USER_EMAIL = 'username@domain.com';
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Badge, Button, MailtoLink } from '@edx/paragon';
|
import {
|
||||||
import { DeleteOutline as DeleteOutlineIcon } from '@edx/paragon/icons';
|
Badge,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
IconButtonWithTooltip,
|
||||||
|
MailtoLink,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { DeleteOutline } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { USER_ROLES, BADGE_STATES } from '../../constants';
|
import { USER_ROLES, BADGE_STATES } from '../constants';
|
||||||
|
|
||||||
const CourseTeamMember = ({
|
const CourseTeamMember = ({
|
||||||
userName,
|
userName,
|
||||||
@@ -19,16 +25,17 @@ const CourseTeamMember = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const isAdminRole = role === USER_ROLES.admin;
|
const isAdminRole = role === USER_ROLES.admin;
|
||||||
|
const badgeColor = isAdminRole ? BADGE_STATES.admin : BADGE_STATES.staff;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="course-team-member" data-testid="course-team-member">
|
<div className="course-team-member" data-testid="course-team-member">
|
||||||
<div className="member-info">
|
<div className="member-info">
|
||||||
<Badge variant={isAdminRole ? BADGE_STATES.danger : BADGE_STATES.secondary} className="badge-current-user">
|
<Badge className={`badge-current-user bg-${badgeColor} text-light-100`}>
|
||||||
{isAdminRole
|
{isAdminRole
|
||||||
? intl.formatMessage(messages.roleAdmin)
|
? intl.formatMessage(messages.roleAdmin)
|
||||||
: intl.formatMessage(messages.roleStaff)}
|
: intl.formatMessage(messages.roleStaff)}
|
||||||
{currentUserEmail === email && (
|
{currentUserEmail === email && (
|
||||||
<span className="badge-current-user">{intl.formatMessage(messages.roleYou)}</span>
|
<span className="badge-current-user x-small text-light-500">{intl.formatMessage(messages.roleYou)}</span>
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="member-info-name font-weight-bold">{userName}</span>
|
<span className="member-info-name font-weight-bold">{userName}</span>
|
||||||
@@ -45,13 +52,13 @@ const CourseTeamMember = ({
|
|||||||
>
|
>
|
||||||
{isAdminRole ? intl.formatMessage(messages.removeButton) : intl.formatMessage(messages.addButton)}
|
{isAdminRole ? intl.formatMessage(messages.removeButton) : intl.formatMessage(messages.addButton)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<IconButtonWithTooltip
|
||||||
className="delete-button"
|
src={DeleteOutline}
|
||||||
variant="tertiary"
|
tooltipContent={intl.formatMessage(messages.deleteUserButton)}
|
||||||
size="sm"
|
|
||||||
data-testid="delete-button"
|
|
||||||
iconBefore={DeleteOutlineIcon}
|
|
||||||
onClick={() => onDelete(email)}
|
onClick={() => onDelete(email)}
|
||||||
|
iconAs={Icon}
|
||||||
|
alt={intl.formatMessage(messages.deleteUserButton)}
|
||||||
|
data-testid="delete-button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge-current-user {
|
.badge-current-user {
|
||||||
color: $gray-100;
|
|
||||||
margin-left: .25rem;
|
margin-left: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,15 +48,5 @@
|
|||||||
.member-actions {
|
.member-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacer;
|
gap: $spacer;
|
||||||
|
|
||||||
.delete-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.course-team.member.button.remove',
|
id: 'course-authoring.course-team.member.button.remove',
|
||||||
defaultMessage: 'Remove admin access',
|
defaultMessage: 'Remove admin access',
|
||||||
},
|
},
|
||||||
|
deleteUserButton: {
|
||||||
|
id: 'course-authoring.course-team.member.button.delete',
|
||||||
|
defaultMessage: 'Delete user',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import HelpSidebar from '../../generic/help-sidebar';
|
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CourseTeamSideBar = ({ courseId, isOwnershipHint, isShowInitialSidebar }) => {
|
const CourseTeamSideBar = ({ courseId, isOwnershipHint, isShowInitialSidebar }) => {
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const InfoModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
{modalType === MODAL_TYPES.delete && (
|
{modalType === MODAL_TYPES.delete && (
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onDeleteSubmit();
|
onDeleteSubmit();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteModalTitle: {
|
deleteModalTitle: {
|
||||||
id: 'course-authoring.course-team.member.button.remove',
|
id: 'course-authoring.course-team.member.button.remove',
|
||||||
defaultMessage: 'Are you sure?',
|
defaultMessage: 'Delete course team member',
|
||||||
},
|
},
|
||||||
deleteModalMessage: {
|
deleteModalMessage: {
|
||||||
id: 'course-authoring.course-team.delete-modal.message',
|
id: 'course-authoring.course-team.delete-modal.message',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const getInfoModalSettings = (modalType, currentEmail, errorMessage, courseName,
|
|||||||
return {
|
return {
|
||||||
title: intl.formatMessage(messages.deleteModalTitle),
|
title: intl.formatMessage(messages.deleteModalTitle),
|
||||||
message: intl.formatMessage(messages.deleteModalMessage, { email: currentEmail, courseName }),
|
message: intl.formatMessage(messages.deleteModalMessage, { email: currentEmail, courseName }),
|
||||||
variant: 'danger',
|
variant: '',
|
||||||
closeButtonText: intl.formatMessage(messages.deleteModalCancelButton),
|
closeButtonText: intl.formatMessage(messages.deleteModalCancelButton),
|
||||||
submitButtonText: intl.formatMessage(messages.deleteModalDeleteButton),
|
submitButtonText: intl.formatMessage(messages.deleteModalDeleteButton),
|
||||||
closeButtonVariant: 'tertiary',
|
closeButtonVariant: 'tertiary',
|
||||||
@@ -34,7 +34,7 @@ const getInfoModalSettings = (modalType, currentEmail, errorMessage, courseName,
|
|||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
closeButtonText: intl.formatMessage(messages.errorModalOkButton),
|
closeButtonText: intl.formatMessage(messages.errorModalOkButton),
|
||||||
closeButtonVariant: 'danger',
|
closeButtonVariant: 'primary',
|
||||||
};
|
};
|
||||||
case MODAL_TYPES.warning:
|
case MODAL_TYPES.warning:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { Add as AddIcon } from '@edx/paragon/icons';
|
import { Add as AddIcon } from '@edx/paragon/icons';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { useModel } from '../generic/model-store';
|
||||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||||
import ProcessingNotification from '../generic/processing-notification';
|
import ProcessingNotification from '../generic/processing-notification';
|
||||||
import SubHeader from '../generic/sub-header/SubHeader';
|
import SubHeader from '../generic/sub-header/SubHeader';
|
||||||
@@ -23,10 +24,14 @@ import messages from './messages';
|
|||||||
import { useCourseUpdates } from './hooks';
|
import { useCourseUpdates } from './hooks';
|
||||||
import { getLoadingStatuses, getSavingStatuses } from './data/selectors';
|
import { getLoadingStatuses, getSavingStatuses } from './data/selectors';
|
||||||
import { matchesAnyStatus } from './utils';
|
import { matchesAnyStatus } from './utils';
|
||||||
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
|
||||||
const CourseUpdates = ({ courseId }) => {
|
const CourseUpdates = ({ courseId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requestType,
|
requestType,
|
||||||
courseUpdates,
|
courseUpdates,
|
||||||
@@ -58,7 +63,7 @@ const CourseUpdates = ({ courseId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container size="xl" className="m-4">
|
<Container size="xl" className="px-4">
|
||||||
<section className="setting-items mb-4 mt-5">
|
<section className="setting-items mb-4 mt-5">
|
||||||
<Layout
|
<Layout
|
||||||
lg={[{ span: 12 }]}
|
lg={[{ span: 12 }]}
|
||||||
@@ -76,7 +81,7 @@ const CourseUpdates = ({ courseId }) => {
|
|||||||
instruction={intl.formatMessage(messages.sectionInfo)}
|
instruction={intl.formatMessage(messages.sectionInfo)}
|
||||||
headerActions={(
|
headerActions={(
|
||||||
<Button
|
<Button
|
||||||
variant="outline-primary"
|
variant="primary"
|
||||||
iconBefore={AddIcon}
|
iconBefore={AddIcon}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleOpenUpdateForm(REQUEST_TYPES.add_new_update)}
|
onClick={() => handleOpenUpdateForm(REQUEST_TYPES.add_new_update)}
|
||||||
|
|||||||
@@ -163,37 +163,39 @@ describe('<CourseUpdates />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Add new update form is visible after clicking "New update" button', async () => {
|
it('Add new update form is visible after clicking "New update" button', async () => {
|
||||||
const { getByText, getByRole, getAllByRole } = render(<RootWrapper />);
|
const { getByText, getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const editButtons = getAllByRole('button', { name: 'Edit' });
|
const editUpdateButtons = getAllByTestId('course-update-edit-button');
|
||||||
const deleteButtons = getAllByRole('button', { name: 'Delete' });
|
const deleteButtons = getAllByTestId('course-update-delete-button');
|
||||||
|
const editHandoutsButtons = getAllByTestId('course-handouts-edit-button');
|
||||||
const newUpdateButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage });
|
const newUpdateButton = getByRole('button', { name: messages.newUpdateButton.defaultMessage });
|
||||||
|
|
||||||
fireEvent.click(newUpdateButton);
|
fireEvent.click(newUpdateButton);
|
||||||
|
|
||||||
expect(newUpdateButton).toBeDisabled();
|
expect(newUpdateButton).toBeDisabled();
|
||||||
editButtons.forEach((button) => expect(button).toBeDisabled());
|
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
|
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
expect(getByText('Add new update')).toBeInTheDocument();
|
expect(getByText('Add new update')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Edit handouts form is visible after clicking "Edit" button', async () => {
|
it('Edit handouts form is visible after clicking "Edit" button', async () => {
|
||||||
const {
|
const { getByText, getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||||
getByText, getByRole, getByTestId, getAllByRole,
|
|
||||||
} = render(<RootWrapper />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const editHandoutsButton = getByTestId('course-handouts-edit-button');
|
const editUpdateButtons = getAllByTestId('course-update-edit-button');
|
||||||
const editButtons = getAllByRole('button', { name: 'Edit' });
|
const deleteButtons = getAllByTestId('course-update-delete-button');
|
||||||
const deleteButtons = getAllByRole('button', { name: 'Delete' });
|
const editHandoutsButtons = getAllByTestId('course-handouts-edit-button');
|
||||||
|
const editHandoutsButton = editHandoutsButtons[0];
|
||||||
|
|
||||||
fireEvent.click(editHandoutsButton);
|
fireEvent.click(editHandoutsButton);
|
||||||
|
|
||||||
expect(editHandoutsButton).toBeDisabled();
|
expect(editHandoutsButton).toBeDisabled();
|
||||||
expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
||||||
editButtons.forEach((button) => expect(button).toBeDisabled());
|
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
|
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
expect(getByText('Edit handouts')).toBeInTheDocument();
|
expect(getByText('Edit handouts')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -201,18 +203,20 @@ describe('<CourseUpdates />', () => {
|
|||||||
|
|
||||||
it('Edit update form is visible after clicking "Edit" button', async () => {
|
it('Edit update form is visible after clicking "Edit" button', async () => {
|
||||||
const {
|
const {
|
||||||
getByText, getByRole, getAllByTestId, getAllByRole, queryByText,
|
getByText, getByRole, getAllByTestId, queryByText,
|
||||||
} = render(<RootWrapper />);
|
} = render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const editUpdateFirstButton = getAllByTestId('course-update-edit-button')[0];
|
const editUpdateButtons = getAllByTestId('course-update-edit-button');
|
||||||
const editButtons = getAllByRole('button', { name: 'Edit' });
|
const deleteButtons = getAllByTestId('course-update-delete-button');
|
||||||
const deleteButtons = getAllByRole('button', { name: 'Delete' });
|
const editHandoutsButtons = getAllByTestId('course-handouts-edit-button');
|
||||||
|
const editUpdateFirstButton = editUpdateButtons[0];
|
||||||
|
|
||||||
fireEvent.click(editUpdateFirstButton);
|
fireEvent.click(editUpdateFirstButton);
|
||||||
expect(getByText('Edit update')).toBeInTheDocument();
|
expect(getByText('Edit update')).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
expect(getByRole('button', { name: messages.newUpdateButton.defaultMessage })).toBeDisabled();
|
||||||
editButtons.forEach((button) => expect(button).toBeDisabled());
|
editUpdateButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
|
editHandoutsButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
deleteButtons.forEach((button) => expect(button).toBeDisabled());
|
||||||
expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
expect(queryByText(courseUpdatesMock[0].content)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button } from '@edx/paragon';
|
import { Icon, IconButtonWithTooltip } from '@edx/paragon';
|
||||||
|
import { EditOutline } from '@edx/paragon/icons';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -12,16 +13,14 @@ const CourseHandouts = ({ contentForHandouts, onEdit, isDisabledButtons }) => {
|
|||||||
<div className="course-handouts" data-testid="course-handouts">
|
<div className="course-handouts" data-testid="course-handouts">
|
||||||
<div className="course-handouts-header">
|
<div className="course-handouts-header">
|
||||||
<h2 className="course-handouts-header__title lead">{intl.formatMessage(messages.handoutsTitle)}</h2>
|
<h2 className="course-handouts-header__title lead">{intl.formatMessage(messages.handoutsTitle)}</h2>
|
||||||
<Button
|
<IconButtonWithTooltip
|
||||||
className="course-handouts-header__btn"
|
tooltipContent={intl.formatMessage(messages.editButton)}
|
||||||
data-testid="course-handouts-edit-button"
|
src={EditOutline}
|
||||||
variant="outline-primary"
|
iconAs={Icon}
|
||||||
size="sm"
|
|
||||||
onClick={onEdit}
|
|
||||||
disabled={isDisabledButtons}
|
disabled={isDisabledButtons}
|
||||||
>
|
data-testid="course-handouts-edit-button"
|
||||||
{intl.formatMessage(messages.editButton)}
|
onClick={onEdit}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="small"
|
className="small"
|
||||||
|
|||||||
@@ -21,25 +21,25 @@ const renderComponent = (props) => render(
|
|||||||
|
|
||||||
describe('<CourseHandouts />', () => {
|
describe('<CourseHandouts />', () => {
|
||||||
it('render CourseHandouts component correctly', () => {
|
it('render CourseHandouts component correctly', () => {
|
||||||
const { getByText, getByRole } = renderComponent();
|
const { getByText, getByTestId } = renderComponent();
|
||||||
|
|
||||||
expect(getByText(messages.handoutsTitle.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.handoutsTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(handoutsContentMock)).toBeInTheDocument();
|
expect(getByText(handoutsContentMock)).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeInTheDocument();
|
expect(getByTestId('course-handouts-edit-button')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onEdit function when the edit button is clicked', () => {
|
it('calls the onEdit function when the edit button is clicked', () => {
|
||||||
const { getByRole } = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
const editButton = getByRole('button', { name: messages.editButton.defaultMessage });
|
const editButton = getByTestId('course-handouts-edit-button');
|
||||||
fireEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
expect(onEditMock).toHaveBeenCalledTimes(1);
|
expect(onEditMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('"Edit" button is disabled when isDisabledButtons is true', () => {
|
it('"Edit" button is disabled when isDisabledButtons is true', () => {
|
||||||
const { getByRole } = renderComponent({ isDisabledButtons: true });
|
const { getByTestId } = renderComponent({ isDisabledButtons: true });
|
||||||
|
|
||||||
const editButton = getByRole('button', { name: messages.editButton.defaultMessage });
|
const editButton = getByTestId('course-handouts-edit-button');
|
||||||
expect(editButton).toBeDisabled();
|
expect(editButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Icon } from '@edx/paragon';
|
import { Icon, IconButtonWithTooltip } from '@edx/paragon';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Error as ErrorIcon } from '@edx/paragon/icons/es5';
|
import { DeleteOutline, EditOutline, Error as ErrorIcon } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import { isDateForUpdateValid } from './utils';
|
import { isDateForUpdateValid } from './utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -27,18 +27,22 @@ const CourseUpdate = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="course-update-header__action">
|
<div className="course-update-header__action">
|
||||||
<Button
|
<IconButtonWithTooltip
|
||||||
variant="outline-primary"
|
tooltipContent={intl.formatMessage(messages.editButton)}
|
||||||
size="sm"
|
src={EditOutline}
|
||||||
onClick={onEdit}
|
iconAs={Icon}
|
||||||
disabled={isDisabledButtons}
|
disabled={isDisabledButtons}
|
||||||
data-testid="course-update-edit-button"
|
data-testid="course-update-edit-button"
|
||||||
>
|
onClick={onEdit}
|
||||||
{intl.formatMessage(messages.editButton)}
|
/>
|
||||||
</Button>
|
<IconButtonWithTooltip
|
||||||
<Button variant="outline-primary" size="sm" onClick={onDelete} disabled={isDisabledButtons}>
|
tooltipContent={intl.formatMessage(messages.deleteButton)}
|
||||||
{intl.formatMessage(messages.deleteButton)}
|
src={DeleteOutline}
|
||||||
</Button>
|
iconAs={Icon}
|
||||||
|
disabled={isDisabledButtons}
|
||||||
|
data-testid="course-update-delete-button"
|
||||||
|
onClick={onDelete}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{Boolean(contentForUpdate) && (
|
{Boolean(contentForUpdate) && (
|
||||||
|
|||||||
@@ -25,20 +25,20 @@ const renderComponent = (props) => render(
|
|||||||
|
|
||||||
describe('<CourseUpdate />', () => {
|
describe('<CourseUpdate />', () => {
|
||||||
it('render CourseUpdate component correctly', () => {
|
it('render CourseUpdate component correctly', () => {
|
||||||
const { getByText, getByRole } = renderComponent();
|
const { getByText, getByTestId } = renderComponent();
|
||||||
|
|
||||||
expect(getByText(dateForUpdateMock)).toBeInTheDocument();
|
expect(getByText(dateForUpdateMock)).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeInTheDocument();
|
expect(getByTestId('course-update-edit-button')).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.deleteButton.defaultMessage })).toBeInTheDocument();
|
expect(getByTestId('course-update-delete-button')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render CourseUpdate component without content correctly', () => {
|
it('render CourseUpdate component without content correctly', () => {
|
||||||
const { getByText, queryByTestId, getByRole } = renderComponent({ contentForUpdate: '' });
|
const { getByText, queryByTestId, getByTestId } = renderComponent({ contentForUpdate: '' });
|
||||||
|
|
||||||
expect(getByText(dateForUpdateMock)).toBeInTheDocument();
|
expect(getByText(dateForUpdateMock)).toBeInTheDocument();
|
||||||
expect(queryByTestId('course-update-content')).not.toBeInTheDocument();
|
expect(queryByTestId('course-update-content')).not.toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeInTheDocument();
|
expect(getByTestId('course-update-edit-button')).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.deleteButton.defaultMessage })).toBeInTheDocument();
|
expect(getByTestId('course-update-delete-button')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render error message when dateForUpdate is inValid', () => {
|
it('render error message when dateForUpdate is inValid', () => {
|
||||||
@@ -48,25 +48,25 @@ describe('<CourseUpdate />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onEdit function when the "Edit" button is clicked', () => {
|
it('calls the onEdit function when the "Edit" button is clicked', () => {
|
||||||
const { getByRole } = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
const editButton = getByRole('button', { name: messages.editButton.defaultMessage });
|
const editButton = getByTestId('course-update-edit-button');
|
||||||
fireEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
expect(onEditMock).toHaveBeenCalledTimes(1);
|
expect(onEditMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onDelete function when the "Delete" button is clicked', () => {
|
it('calls the onDelete function when the "Delete" button is clicked', () => {
|
||||||
const { getByRole } = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
const deleteButton = getByRole('button', { name: messages.deleteButton.defaultMessage });
|
const deleteButton = getByTestId('course-update-delete-button');
|
||||||
fireEvent.click(deleteButton);
|
fireEvent.click(deleteButton);
|
||||||
expect(onDeleteMock).toHaveBeenCalledTimes(1);
|
expect(onDeleteMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('"Edit" and "Delete" buttons is disabled when isDisabledButtons is true', () => {
|
it('"Edit" and "Delete" buttons is disabled when isDisabledButtons is true', () => {
|
||||||
const { getByRole } = renderComponent({ isDisabledButtons: true });
|
const { getByTestId } = renderComponent({ isDisabledButtons: true });
|
||||||
|
|
||||||
expect(getByRole('button', { name: messages.editButton.defaultMessage })).toBeDisabled();
|
expect(getByTestId('course-update-edit-button')).toBeDisabled();
|
||||||
expect(getByRole('button', { name: messages.deleteButton.defaultMessage })).toBeDisabled();
|
expect(getByTestId('course-update-delete-button')).toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const DeleteModal = ({ isOpen, close, onDeleteSubmit }) => {
|
|||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
title={intl.formatMessage(messages.deleteModalTitle)}
|
title={intl.formatMessage(messages.deleteModalTitle)}
|
||||||
variant="warning"
|
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
footerNode={(
|
footerNode={(
|
||||||
@@ -29,7 +28,7 @@ const DeleteModal = ({ isOpen, close, onDeleteSubmit }) => {
|
|||||||
onDeleteSubmit();
|
onDeleteSubmit();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.okButton)}
|
{intl.formatMessage(messages.deleteButton)}
|
||||||
</Button>
|
</Button>
|
||||||
</ActionRow>
|
</ActionRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ describe('<DeleteModal />', () => {
|
|||||||
expect(getByText(messages.deleteModalTitle.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.deleteModalTitle.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByText(messages.deleteModalDescription.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.deleteModalDescription.defaultMessage)).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: messages.okButton.defaultMessage })).toBeInTheDocument();
|
expect(getByRole('button', { name: messages.deleteButton.defaultMessage })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onDeleteSubmit function when the "Ok" button is clicked', () => {
|
it('calls onDeleteSubmit function when the "Ok" button is clicked', () => {
|
||||||
const { getByRole } = renderComponent();
|
const { getByRole } = renderComponent();
|
||||||
|
|
||||||
const okButton = getByRole('button', { name: messages.okButton.defaultMessage });
|
const deleteButton = getByRole('button', { name: messages.deleteButton.defaultMessage });
|
||||||
fireEvent.click(okButton);
|
fireEvent.click(deleteButton);
|
||||||
expect(onDeleteSubmitMock).toHaveBeenCalledTimes(1);
|
expect(onDeleteSubmitMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.course-updates.actions.cancel',
|
id: 'course-authoring.course-updates.actions.cancel',
|
||||||
defaultMessage: 'Cancel',
|
defaultMessage: 'Cancel',
|
||||||
},
|
},
|
||||||
okButton: {
|
deleteButton: {
|
||||||
id: 'course-authoring.course-updates.button.ok',
|
id: 'course-authoring.course-updates.button.delete',
|
||||||
defaultMessage: 'Ok',
|
defaultMessage: 'Delete',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
sectionInfo: {
|
sectionInfo: {
|
||||||
id: 'course-authoring.course-updates.section-info',
|
id: 'course-authoring.course-updates.section-info',
|
||||||
defaultMessage: 'Use course updates to notify students of important dates or exams, highlight particular discussions in the forums, announce schedule changes, and respond to student questions. You add or edit updates in HTML.',
|
defaultMessage: 'Use course updates to notify students of important dates or exams, highlight particular discussions in the forums, announce schedule changes, and respond to student questions.',
|
||||||
},
|
},
|
||||||
newUpdateButton: {
|
newUpdateButton: {
|
||||||
id: 'course-authoring.course-updates.actions.new-update',
|
id: 'course-authoring.course-updates.actions.new-update',
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const UpdateForm = ({
|
|||||||
</div>
|
</div>
|
||||||
{!isValid && (
|
{!isValid && (
|
||||||
<div className="datepicker-field-error">
|
<div className="datepicker-field-error">
|
||||||
<Icon src={ErrorIcon} alt={intl.formatMessage(messages.updateFormErrorAltText)} />
|
<Icon src={ErrorIcon} className="text-danger-500" alt={intl.formatMessage(messages.updateFormErrorAltText)} />
|
||||||
<span className="message-error">{intl.formatMessage(messages.updateFormInValid)}</span>
|
<span className="message-error">{intl.formatMessage(messages.updateFormInValid)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,10 +32,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .25rem;
|
gap: .25rem;
|
||||||
|
|
||||||
svg {
|
|
||||||
color: $warning-300;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker-popper {
|
.react-datepicker-popper {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import Placeholder, {
|
|||||||
} from '@edx/frontend-lib-content-components';
|
} from '@edx/frontend-lib-content-components';
|
||||||
|
|
||||||
import { RequestStatus } from '../data/constants';
|
import { RequestStatus } from '../data/constants';
|
||||||
import { useModels } from '../generic/model-store';
|
import { useModels, useModel } from '../generic/model-store';
|
||||||
import { getLoadingStatus, getSavingStatus } from './data/selectors';
|
import { getLoadingStatus, getSavingStatus } from './data/selectors';
|
||||||
import {
|
import {
|
||||||
addSingleCustomPage,
|
addSingleCustomPage,
|
||||||
@@ -40,6 +40,8 @@ import CustomPageCard from './CustomPageCard';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import CustomPagesProvider from './CustomPagesProvider';
|
import CustomPagesProvider from './CustomPagesProvider';
|
||||||
import EditModal from './EditModal';
|
import EditModal from './EditModal';
|
||||||
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
import { getPagePath } from '../utils';
|
||||||
|
|
||||||
const CustomPages = ({
|
const CustomPages = ({
|
||||||
courseId,
|
courseId,
|
||||||
@@ -52,6 +54,9 @@ const CustomPages = ({
|
|||||||
const [isOpen, open, close] = useToggle(false);
|
const [isOpen, open, close] = useToggle(false);
|
||||||
const [isEditModalOpen, openEditModal, closeEditModal] = useToggle(false);
|
const [isEditModalOpen, openEditModal, closeEditModal] = useToggle(false);
|
||||||
|
|
||||||
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||||
|
|
||||||
const { config } = useContext(AppContext);
|
const { config } = useContext(AppContext);
|
||||||
const { path, url } = useRouteMatch();
|
const { path, url } = useRouteMatch();
|
||||||
const learningCourseURL = `${config.LEARNING_BASE_URL}/course/${courseId}`;
|
const learningCourseURL = `${config.LEARNING_BASE_URL}/course/${courseId}`;
|
||||||
@@ -117,7 +122,7 @@ const CustomPages = ({
|
|||||||
ariaLabel="Custom Page breadcrumbs"
|
ariaLabel="Custom Page breadcrumbs"
|
||||||
links={[
|
links={[
|
||||||
{ label: 'Content', href: `${config.STUDIO_BASE_URL}/course/${courseId}` },
|
{ label: 'Content', href: `${config.STUDIO_BASE_URL}/course/${courseId}` },
|
||||||
{ label: 'Pages and Resources', href: `/course/${courseId}/pages-and-resources` },
|
{ label: 'Pages and Resources', href: getPagePath(courseId, 'true', 'tabs') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const RequestStatus = {
|
|||||||
FAILED: 'failed',
|
FAILED: 'failed',
|
||||||
DENIED: 'denied',
|
DENIED: 'denied',
|
||||||
PENDING: 'pending',
|
PENDING: 'pending',
|
||||||
|
CLEAR: 'clear',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
127
src/export-page/CourseExportPage.jsx
Normal file
127
src/export-page/CourseExportPage.jsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import {
|
||||||
|
Container, Layout, Button, Card,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { ArrowCircleDown as ArrowCircleDownIcon } from '@edx/paragon/icons';
|
||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||||
|
import SubHeader from '../generic/sub-header/SubHeader';
|
||||||
|
import { RequestStatus } from '../data/constants';
|
||||||
|
import { useModel } from '../generic/model-store';
|
||||||
|
import messages from './messages';
|
||||||
|
import ExportSidebar from './export-sidebar/ExportSidebar';
|
||||||
|
import {
|
||||||
|
getCurrentStage, getError, getExportTriggered, getLoadingStatus, getSavingStatus,
|
||||||
|
} from './data/selectors';
|
||||||
|
import { startExportingCourse } from './data/thunks';
|
||||||
|
import { EXPORT_STAGES, LAST_EXPORT_COOKIE_NAME } from './data/constants';
|
||||||
|
import { updateExportTriggered, updateSavingStatus, updateSuccessDate } from './data/slice';
|
||||||
|
import ExportModalError from './export-modal-error/ExportModalError';
|
||||||
|
import ExportFooter from './export-footer/ExportFooter';
|
||||||
|
import ExportStepper from './export-stepper/ExportStepper';
|
||||||
|
|
||||||
|
const CourseExportPage = ({ intl, courseId }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const exportTriggered = useSelector(getExportTriggered);
|
||||||
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
|
const currentStage = useSelector(getCurrentStage);
|
||||||
|
const { msg: errorMessage } = useSelector(getError);
|
||||||
|
const loadingStatus = useSelector(getLoadingStatus);
|
||||||
|
const savingStatus = useSelector(getSavingStatus);
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS;
|
||||||
|
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
||||||
|
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME);
|
||||||
|
if (cookieData) {
|
||||||
|
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
|
dispatch(updateExportTriggered(true));
|
||||||
|
dispatch(updateSuccessDate(cookieData.date));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{intl.formatMessage(messages.pageTitle, {
|
||||||
|
headingTitle: intl.formatMessage(messages.headingTitle),
|
||||||
|
courseName: courseDetails?.name,
|
||||||
|
siteName: process.env.SITE_NAME,
|
||||||
|
})}
|
||||||
|
</title>
|
||||||
|
</Helmet>
|
||||||
|
<Container size="xl" className="mt-4 px-4 export">
|
||||||
|
<section className="setting-items mb-4">
|
||||||
|
<Layout
|
||||||
|
lg={[{ span: 9 }, { span: 3 }]}
|
||||||
|
md={[{ span: 9 }, { span: 3 }]}
|
||||||
|
sm={[{ span: 9 }, { span: 3 }]}
|
||||||
|
xs={[{ span: 9 }, { span: 3 }]}
|
||||||
|
xl={[{ span: 9 }, { span: 3 }]}
|
||||||
|
>
|
||||||
|
<Layout.Element>
|
||||||
|
<article>
|
||||||
|
<SubHeader
|
||||||
|
title={intl.formatMessage(messages.headingTitle)}
|
||||||
|
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||||
|
/>
|
||||||
|
<p className="small">{intl.formatMessage(messages.description1, { studioShortName: getConfig().STUDIO_SHORT_NAME })}</p>
|
||||||
|
<p className="small">{intl.formatMessage(messages.description2)}</p>
|
||||||
|
<Card>
|
||||||
|
<Card.Header
|
||||||
|
className="h3 px-3 text-black mb-4"
|
||||||
|
title={intl.formatMessage(messages.titleUnderButton)}
|
||||||
|
/>
|
||||||
|
{isShowExportButton && (
|
||||||
|
<Card.Section className="px-3 py-1">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
className="mb-4"
|
||||||
|
onClick={() => dispatch(startExportingCourse(courseId))}
|
||||||
|
iconBefore={ArrowCircleDownIcon}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.buttonTitle)}
|
||||||
|
</Button>
|
||||||
|
</Card.Section>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
{exportTriggered && <ExportStepper courseId={courseId} />}
|
||||||
|
<ExportFooter />
|
||||||
|
</article>
|
||||||
|
</Layout.Element>
|
||||||
|
<Layout.Element>
|
||||||
|
<ExportSidebar courseId={courseId} />
|
||||||
|
</Layout.Element>
|
||||||
|
</Layout>
|
||||||
|
</section>
|
||||||
|
<ExportModalError courseId={courseId} />
|
||||||
|
</Container>
|
||||||
|
<div className="alert-toast">
|
||||||
|
<InternetConnectionAlert
|
||||||
|
isFailed={anyRequestFailed}
|
||||||
|
isQueryPending={anyRequestInProgress}
|
||||||
|
onInternetConnectionFailed={() => null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CourseExportPage.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
courseId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CourseExportPage.defaultProps = {};
|
||||||
|
|
||||||
|
export default injectIntl(CourseExportPage);
|
||||||
7
src/export-page/CourseExportPage.scss
Normal file
7
src/export-page/CourseExportPage.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@import "./export-footer/ExportFooter";
|
||||||
|
|
||||||
|
.export {
|
||||||
|
.help-sidebar {
|
||||||
|
margin-top: 7.188rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/export-page/CourseExportPage.test.jsx
Normal file
140
src/export-page/CourseExportPage.test.jsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import initializeStore from '../store';
|
||||||
|
import stepperMessages from './export-stepper/messages';
|
||||||
|
import modalErrorMessages from './export-modal-error/messages';
|
||||||
|
import { getExportStatusApiUrl, postExportCourseApiUrl } from './data/api';
|
||||||
|
import { EXPORT_STAGES } from './data/constants';
|
||||||
|
import { exportPageMock } from './__mocks__';
|
||||||
|
import messages from './messages';
|
||||||
|
import CourseExportPage from './CourseExportPage';
|
||||||
|
|
||||||
|
let store;
|
||||||
|
let axiosMock;
|
||||||
|
let cookies;
|
||||||
|
const courseId = '123';
|
||||||
|
const courseName = 'About Node JS';
|
||||||
|
|
||||||
|
jest.mock('../generic/model-store', () => ({
|
||||||
|
useModel: jest.fn().mockReturnValue({
|
||||||
|
name: courseName,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('universal-cookie', () => {
|
||||||
|
const mCookie = {
|
||||||
|
get: jest.fn(),
|
||||||
|
set: jest.fn(),
|
||||||
|
};
|
||||||
|
return jest.fn(() => mCookie);
|
||||||
|
});
|
||||||
|
|
||||||
|
const RootWrapper = () => (
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<CourseExportPage intl={injectIntl} courseId={courseId} />
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<CourseExportPage />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store = initializeStore();
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
axiosMock
|
||||||
|
.onGet(postExportCourseApiUrl(courseId))
|
||||||
|
.reply(200, exportPageMock);
|
||||||
|
cookies = new Cookies();
|
||||||
|
cookies.get.mockReturnValue(null);
|
||||||
|
});
|
||||||
|
it('should render page title correctly', async () => {
|
||||||
|
render(<RootWrapper />);
|
||||||
|
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(<RootWrapper />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
const exportPageElement = getByText(messages.headingTitle.defaultMessage, {
|
||||||
|
selector: 'h2.sub-header-title',
|
||||||
|
});
|
||||||
|
expect(exportPageElement).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.titleUnderButton.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should start exporting on click', async () => {
|
||||||
|
const { getByText, container } = render(<RootWrapper />);
|
||||||
|
const button = container.querySelector('.btn-primary');
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should show modal error', async () => {
|
||||||
|
axiosMock
|
||||||
|
.onGet(getExportStatusApiUrl(courseId))
|
||||||
|
.reply(200, { exportStatus: EXPORT_STAGES.EXPORTING, exportError: { rawErrorMsg: 'test error', editUnitUrl: 'http://test-url.test' } });
|
||||||
|
const { getByText, queryByText, container } = render(<RootWrapper />);
|
||||||
|
const startExportButton = container.querySelector('.btn-primary');
|
||||||
|
fireEvent.click(startExportButton);
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((r) => setTimeout(r, 3500));
|
||||||
|
expect(getByText(/There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: test error/i));
|
||||||
|
const closeModalWindowButton = getByText('Return to export');
|
||||||
|
fireEvent.click(closeModalWindowButton);
|
||||||
|
expect(queryByText(modalErrorMessages.errorCancelButtonUnit.defaultMessage)).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(closeModalWindowButton);
|
||||||
|
});
|
||||||
|
it('should fetch status without clicking when cookies has', async () => {
|
||||||
|
cookies.get.mockReturnValue({ date: 1679787000 });
|
||||||
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should show download path for relative path', async () => {
|
||||||
|
axiosMock
|
||||||
|
.onGet(getExportStatusApiUrl(courseId))
|
||||||
|
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: '/test-download-path.test' });
|
||||||
|
const { getByText, container } = render(<RootWrapper />);
|
||||||
|
const startExportButton = container.querySelector('.btn-primary');
|
||||||
|
fireEvent.click(startExportButton);
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((r) => setTimeout(r, 3500));
|
||||||
|
const downloadButton = getByText(stepperMessages.downloadCourseButtonTitle.defaultMessage);
|
||||||
|
expect(downloadButton).toBeInTheDocument();
|
||||||
|
expect(downloadButton.getAttribute('href')).toEqual(`${getConfig().STUDIO_BASE_URL}/test-download-path.test`);
|
||||||
|
});
|
||||||
|
it('should show download path for absolute path', async () => {
|
||||||
|
axiosMock
|
||||||
|
.onGet(getExportStatusApiUrl(courseId))
|
||||||
|
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: 'http://test-download-path.test' });
|
||||||
|
const { getByText, container } = render(<RootWrapper />);
|
||||||
|
const startExportButton = container.querySelector('.btn-primary');
|
||||||
|
fireEvent.click(startExportButton);
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((r) => setTimeout(r, 3500));
|
||||||
|
const downloadButton = getByText(stepperMessages.downloadCourseButtonTitle.defaultMessage);
|
||||||
|
expect(downloadButton).toBeInTheDocument();
|
||||||
|
expect(downloadButton.getAttribute('href')).toEqual('http://test-download-path.test');
|
||||||
|
});
|
||||||
|
});
|
||||||
3
src/export-page/__mocks__/exportPage.js
Normal file
3
src/export-page/__mocks__/exportPage.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
exportStatus: 1,
|
||||||
|
};
|
||||||
2
src/export-page/__mocks__/index.js
Normal file
2
src/export-page/__mocks__/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export { default as exportPageMock } from './exportPage';
|
||||||
19
src/export-page/data/api.js
Normal file
19
src/export-page/data/api.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||||
|
export const postExportCourseApiUrl = (courseId) => new URL(`export/${courseId}`, getApiBaseUrl()).href;
|
||||||
|
export const getExportStatusApiUrl = (courseId) => new URL(`export_status/${courseId}`, getApiBaseUrl()).href;
|
||||||
|
|
||||||
|
export async function startCourseExporting(courseId) {
|
||||||
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
|
.post(postExportCourseApiUrl(courseId));
|
||||||
|
return camelCaseObject(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExportStatus(courseId) {
|
||||||
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
|
.get(getExportStatusApiUrl(courseId));
|
||||||
|
return camelCaseObject(data);
|
||||||
|
}
|
||||||
47
src/export-page/data/api.test.js
Normal file
47
src/export-page/data/api.test.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
import { getExportStatus, postExportCourseApiUrl, startCourseExporting } from './api';
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
const courseId = 'course-123';
|
||||||
|
|
||||||
|
describe('API Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch status on start exporting', async () => {
|
||||||
|
const data = { exportStatus: 1 };
|
||||||
|
axiosMock.onPost(postExportCourseApiUrl(courseId)).reply(200, data);
|
||||||
|
|
||||||
|
const result = await startCourseExporting(courseId);
|
||||||
|
|
||||||
|
expect(axiosMock.history.post[0].url).toEqual(postExportCourseApiUrl(courseId));
|
||||||
|
expect(result).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch on get export status', async () => {
|
||||||
|
const data = { exportStatus: 2 };
|
||||||
|
const queryUrl = new URL(`export_status/${courseId}`, getConfig().STUDIO_BASE_URL).href;
|
||||||
|
axiosMock.onGet(queryUrl).reply(200, data);
|
||||||
|
|
||||||
|
const result = await getExportStatus(courseId);
|
||||||
|
|
||||||
|
expect(axiosMock.history.get[0].url).toEqual(queryUrl);
|
||||||
|
expect(result).toEqual(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
8
src/export-page/data/constants.js
Normal file
8
src/export-page/data/constants.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const LAST_EXPORT_COOKIE_NAME = 'lastexport';
|
||||||
|
export const EXPORT_STAGES = {
|
||||||
|
PREPARING: 0,
|
||||||
|
EXPORTING: 1,
|
||||||
|
COMPRESSING: 2,
|
||||||
|
SUCCESS: 3,
|
||||||
|
};
|
||||||
|
export const SUCCESS_DATE_FORMAT = 'MM/DD/yyyy';
|
||||||
8
src/export-page/data/selectors.js
Normal file
8
src/export-page/data/selectors.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const getExportTriggered = (state) => state.courseExport.exportTriggered;
|
||||||
|
export const getCurrentStage = (state) => state.courseExport.currentStage;
|
||||||
|
export const getDownloadPath = (state) => state.courseExport.downloadPath;
|
||||||
|
export const getSuccessDate = (state) => state.courseExport.successDate;
|
||||||
|
export const getError = (state) => state.courseExport.error;
|
||||||
|
export const getIsErrorModalOpen = (state) => state.courseExport.isErrorModalOpen;
|
||||||
|
export const getLoadingStatus = (state) => state.courseExport.loadingStatus;
|
||||||
|
export const getSavingStatus = (state) => state.courseExport.savingStatus;
|
||||||
63
src/export-page/data/slice.js
Normal file
63
src/export-page/data/slice.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
exportTriggered: false,
|
||||||
|
currentStage: 0,
|
||||||
|
error: { msg: null, unitUrl: null },
|
||||||
|
downloadPath: null,
|
||||||
|
successDate: null,
|
||||||
|
isErrorModalOpen: false,
|
||||||
|
loadingStatus: '',
|
||||||
|
savingStatus: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const slice = createSlice({
|
||||||
|
name: 'exportPage',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
updateExportTriggered: (state, { payload }) => {
|
||||||
|
state.exportTriggered = payload;
|
||||||
|
},
|
||||||
|
updateCurrentStage: (state, { payload }) => {
|
||||||
|
if (payload >= state.currentStage) {
|
||||||
|
state.currentStage = payload;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateDownloadPath: (state, { payload }) => {
|
||||||
|
state.downloadPath = payload;
|
||||||
|
},
|
||||||
|
updateSuccessDate: (state, { payload }) => {
|
||||||
|
state.successDate = payload;
|
||||||
|
},
|
||||||
|
updateError: (state, { payload }) => {
|
||||||
|
state.error = payload;
|
||||||
|
},
|
||||||
|
updateIsErrorModalOpen: (state, { payload }) => {
|
||||||
|
state.isErrorModalOpen = payload;
|
||||||
|
},
|
||||||
|
reset: () => initialState,
|
||||||
|
updateLoadingStatus: (state, { payload }) => {
|
||||||
|
state.loadingStatus = payload.status;
|
||||||
|
},
|
||||||
|
updateSavingStatus: (state, { payload }) => {
|
||||||
|
state.savingStatus = payload.status;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
updateExportTriggered,
|
||||||
|
updateCurrentStage,
|
||||||
|
updateDownloadPath,
|
||||||
|
updateSuccessDate,
|
||||||
|
updateError,
|
||||||
|
updateIsErrorModalOpen,
|
||||||
|
reset,
|
||||||
|
updateLoadingStatus,
|
||||||
|
updateSavingStatus,
|
||||||
|
} = slice.actions;
|
||||||
|
|
||||||
|
export const {
|
||||||
|
reducer,
|
||||||
|
} = slice;
|
||||||
96
src/export-page/data/thunks.js
Normal file
96
src/export-page/data/thunks.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
import { RequestStatus } from '../../data/constants';
|
||||||
|
import { setExportCookie } from '../utils';
|
||||||
|
import { EXPORT_STAGES, LAST_EXPORT_COOKIE_NAME } from './constants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
startCourseExporting,
|
||||||
|
getExportStatus,
|
||||||
|
} from './api';
|
||||||
|
import {
|
||||||
|
updateExportTriggered,
|
||||||
|
updateCurrentStage,
|
||||||
|
updateDownloadPath,
|
||||||
|
updateSuccessDate,
|
||||||
|
updateError,
|
||||||
|
updateIsErrorModalOpen,
|
||||||
|
reset,
|
||||||
|
updateLoadingStatus,
|
||||||
|
updateSavingStatus,
|
||||||
|
} from './slice';
|
||||||
|
|
||||||
|
function setExportDate({
|
||||||
|
date, exportStatus, exportOutput, dispatch,
|
||||||
|
}) {
|
||||||
|
// If there is no cookie for the last export date, set it now.
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME);
|
||||||
|
if (!cookieData?.completed) {
|
||||||
|
setExportCookie(date, exportStatus === EXPORT_STAGES.SUCCESS);
|
||||||
|
}
|
||||||
|
// If we don't have export date set yet via cookie, set success date to current date.
|
||||||
|
if (exportOutput && !cookieData?.completed) {
|
||||||
|
dispatch(updateSuccessDate(date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startExportingCourse(courseId) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||||
|
try {
|
||||||
|
dispatch(reset());
|
||||||
|
dispatch(updateExportTriggered(true));
|
||||||
|
const exportData = await startCourseExporting(courseId);
|
||||||
|
dispatch(updateCurrentStage(exportData.exportStatus));
|
||||||
|
setExportCookie(moment().valueOf(), exportData.exportStatus === EXPORT_STAGES.SUCCESS);
|
||||||
|
|
||||||
|
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportStatus(courseId) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
exportStatus, exportOutput, exportError,
|
||||||
|
} = await getExportStatus(courseId);
|
||||||
|
dispatch(updateCurrentStage(Math.abs(exportStatus)));
|
||||||
|
|
||||||
|
const date = moment().valueOf();
|
||||||
|
|
||||||
|
setExportDate({
|
||||||
|
date, exportStatus, exportOutput, dispatch,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exportError) {
|
||||||
|
const errorMessage = exportError.rawErrorMsg || exportError;
|
||||||
|
const errorUnitUrl = exportError.editUnitUrl || null;
|
||||||
|
dispatch(updateError({ msg: errorMessage, unitUrl: errorUnitUrl }));
|
||||||
|
dispatch(updateIsErrorModalOpen(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportOutput) {
|
||||||
|
if (exportOutput.startsWith('/')) {
|
||||||
|
dispatch(updateDownloadPath(`${getConfig().STUDIO_BASE_URL}${exportOutput}`));
|
||||||
|
} else {
|
||||||
|
dispatch(updateDownloadPath(exportOutput));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(updateLoadingStatus({ status: RequestStatus.FAILED }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
146
src/export-page/data/thunks.test.js
Normal file
146
src/export-page/data/thunks.test.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import { fetchExportStatus } from './thunks';
|
||||||
|
import * as api from './api';
|
||||||
|
import { EXPORT_STAGES } from './constants';
|
||||||
|
|
||||||
|
jest.mock('universal-cookie', () => jest.fn().mockImplementation(() => ({
|
||||||
|
get: jest.fn().mockImplementation(() => ({ completed: false })),
|
||||||
|
})));
|
||||||
|
|
||||||
|
jest.mock('../utils', () => ({
|
||||||
|
setExportCookie: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('fetchExportStatus thunk', () => {
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
const getState = jest.fn();
|
||||||
|
const courseId = 'course-123';
|
||||||
|
const exportStatus = EXPORT_STAGES.COMPRESSING;
|
||||||
|
const exportOutput = 'export output';
|
||||||
|
const exportError = 'export error';
|
||||||
|
let mockGetExportStatus;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
mockGetExportStatus = jest.spyOn(api, 'getExportStatus').mockResolvedValue({
|
||||||
|
exportStatus,
|
||||||
|
exportOutput,
|
||||||
|
exportError,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch updateCurrentStage with export status', async () => {
|
||||||
|
mockGetExportStatus.mockResolvedValue({
|
||||||
|
exportStatus,
|
||||||
|
exportOutput,
|
||||||
|
exportError,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchExportStatus(courseId)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
payload: exportStatus,
|
||||||
|
type: 'exportPage/updateCurrentStage',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch updateError on export error', async () => {
|
||||||
|
mockGetExportStatus.mockResolvedValue({
|
||||||
|
exportStatus,
|
||||||
|
exportOutput,
|
||||||
|
exportError,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchExportStatus(courseId)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
payload: {
|
||||||
|
msg: exportError,
|
||||||
|
unitUrl: null,
|
||||||
|
},
|
||||||
|
type: 'exportPage/updateError',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch updateIsErrorModalOpen with true if export error', async () => {
|
||||||
|
mockGetExportStatus.mockResolvedValue({
|
||||||
|
exportStatus,
|
||||||
|
exportOutput,
|
||||||
|
exportError,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchExportStatus(courseId)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
payload: true,
|
||||||
|
type: 'exportPage/updateIsErrorModalOpen',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch updateIsErrorModalOpen if no export error', async () => {
|
||||||
|
mockGetExportStatus.mockResolvedValue({
|
||||||
|
exportStatus,
|
||||||
|
exportOutput,
|
||||||
|
exportError: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchExportStatus(courseId)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch).not.toHaveBeenCalledWith({
|
||||||
|
payload: false,
|
||||||
|
type: 'exportPage/updateIsErrorModalOpen',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch updateDownloadPath if there's export output", async () => {
|
||||||
|
mockGetExportStatus.mockResolvedValue({
|
||||||
|
exportStatus,
|
||||||
|
exportOutput,
|
||||||
|
exportError,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchExportStatus(courseId)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
payload: exportOutput,
|
||||||
|
type: 'exportPage/updateDownloadPath',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch updateSuccessDate with current date if export status is success', async () => {
|
||||||
|
mockGetExportStatus.mockResolvedValue({
|
||||||
|
exportStatus:
|
||||||
|
EXPORT_STAGES.SUCCESS,
|
||||||
|
exportOutput,
|
||||||
|
exportError,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchExportStatus(courseId)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
payload: expect.any(Number),
|
||||||
|
type: 'exportPage/updateSuccessDate',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch updateSuccessDate with current date if last-export cookie is already set', async () => {
|
||||||
|
mockGetExportStatus.mockResolvedValue({
|
||||||
|
exportStatus:
|
||||||
|
EXPORT_STAGES.SUCCESS,
|
||||||
|
exportOutput,
|
||||||
|
exportError,
|
||||||
|
});
|
||||||
|
|
||||||
|
Cookies.mockImplementation(() => ({
|
||||||
|
get: jest.fn().mockReturnValueOnce({ completed: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await fetchExportStatus(courseId)(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch).not.toHaveBeenCalledWith({
|
||||||
|
payload: expect.any,
|
||||||
|
type: 'exportPage/updateSuccessDate',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/export-page/export-footer/ExportFooter.jsx
Normal file
49
src/export-page/export-footer/ExportFooter.jsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
injectIntl,
|
||||||
|
intlShape,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
|
import { Layout } from '@edx/paragon';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const ExportFooter = ({ intl }) => (
|
||||||
|
<footer className="mt-4 small">
|
||||||
|
<Layout
|
||||||
|
lg={[{ span: 5 }, { span: 2 }, { span: 5 }]}
|
||||||
|
md={[{ span: 5 }, { span: 2 }, { span: 5 }]}
|
||||||
|
sm={[{ span: 5 }, { span: 2 }, { span: 5 }]}
|
||||||
|
xs={[{ span: 5 }, { span: 2 }, { span: 5 }]}
|
||||||
|
xl={[{ span: 5 }, { span: 2 }, { span: 5 }]}
|
||||||
|
>
|
||||||
|
<Layout.Element>
|
||||||
|
<h4>{intl.formatMessage(messages.exportedDataTitle)}</h4>
|
||||||
|
<ul className="export-footer-list">
|
||||||
|
<li>{intl.formatMessage(messages.exportedDataItem1)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.exportedDataItem2)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.exportedDataItem3)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.exportedDataItem4)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.exportedDataItem5)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.exportedDataItem6)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.exportedDataItem7)}</li>
|
||||||
|
</ul>
|
||||||
|
</Layout.Element>
|
||||||
|
<Layout.Element />
|
||||||
|
<Layout.Element>
|
||||||
|
<h4>{intl.formatMessage(messages.notExportedDataTitle)}</h4>
|
||||||
|
<ul className="export-footer-list">
|
||||||
|
<li>{intl.formatMessage(messages.notExportedDataItem1)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.notExportedDataItem2)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.notExportedDataItem3)}</li>
|
||||||
|
<li>{intl.formatMessage(messages.notExportedDataItem4)}</li>
|
||||||
|
</ul>
|
||||||
|
</Layout.Element>
|
||||||
|
</Layout>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
|
||||||
|
ExportFooter.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ExportFooter);
|
||||||
14
src/export-page/export-footer/ExportFooter.scss
Normal file
14
src/export-page/export-footer/ExportFooter.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.export-footer-list {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-bottom: .3125rem;
|
||||||
|
border-bottom: 1px solid #E5E5E5;
|
||||||
|
margin-bottom: .3125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/export-page/export-footer/messages.js
Normal file
58
src/export-page/export-footer/messages.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
exportedDataTitle: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.title',
|
||||||
|
defaultMessage: 'Data exported with your course:',
|
||||||
|
},
|
||||||
|
exportedDataItem1: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.item.1',
|
||||||
|
defaultMessage: 'Values from Advanced settings, including MATLAB API keys and LTI passports',
|
||||||
|
},
|
||||||
|
exportedDataItem2: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.item.2',
|
||||||
|
defaultMessage: 'Course content (all sections, sub-sections, and units)',
|
||||||
|
},
|
||||||
|
exportedDataItem3: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.item.3',
|
||||||
|
defaultMessage: 'Course structure',
|
||||||
|
},
|
||||||
|
exportedDataItem4: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.item.4',
|
||||||
|
defaultMessage: 'Individual problems',
|
||||||
|
},
|
||||||
|
exportedDataItem5: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.item.5',
|
||||||
|
defaultMessage: 'Pages',
|
||||||
|
},
|
||||||
|
exportedDataItem6: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.item.6',
|
||||||
|
defaultMessage: 'Course assets',
|
||||||
|
},
|
||||||
|
exportedDataItem7: {
|
||||||
|
id: 'course-authoring.export.footer.exportedData.item.7',
|
||||||
|
defaultMessage: 'Course settings',
|
||||||
|
},
|
||||||
|
notExportedDataTitle: {
|
||||||
|
id: 'course-authoring.export.footer.notExportedData.title',
|
||||||
|
defaultMessage: 'Data not exported with your course:',
|
||||||
|
},
|
||||||
|
notExportedDataItem1: {
|
||||||
|
id: 'course-authoring.export.footer.notExportedData.item.1',
|
||||||
|
defaultMessage: 'User data',
|
||||||
|
},
|
||||||
|
notExportedDataItem2: {
|
||||||
|
id: 'course-authoring.export.footer.notExportedData.item.2',
|
||||||
|
defaultMessage: 'Course team data',
|
||||||
|
},
|
||||||
|
notExportedDataItem3: {
|
||||||
|
id: 'course-authoring.export.footer.notExportedData.item.3',
|
||||||
|
defaultMessage: 'Forum/discussion data',
|
||||||
|
},
|
||||||
|
notExportedDataItem4: {
|
||||||
|
id: 'course-authoring.export.footer.notExportedData.item.4',
|
||||||
|
defaultMessage: 'Certificates',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
53
src/export-page/export-modal-error/ExportModalError.jsx
Normal file
53
src/export-page/export-modal-error/ExportModalError.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import ModalError from '../../generic/modal-error/ModalError';
|
||||||
|
import { getError, getIsErrorModalOpen } from '../data/selectors';
|
||||||
|
import { updateIsErrorModalOpen } from '../data/slice';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const ExportModalError = ({
|
||||||
|
intl,
|
||||||
|
courseId,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isErrorModalOpen = useSelector(getIsErrorModalOpen);
|
||||||
|
const { msg: errorMessage, unitUrl: unitErrorUrl } = useSelector(getError);
|
||||||
|
|
||||||
|
const handleUnitRedirect = () => { window.location.assign(unitErrorUrl); };
|
||||||
|
const handleRedirectCourseHome = () => { window.location.assign(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`); };
|
||||||
|
return (
|
||||||
|
<ModalError
|
||||||
|
isOpen={isErrorModalOpen}
|
||||||
|
title={intl.formatMessage(messages.errorTitle)}
|
||||||
|
message={
|
||||||
|
intl.formatMessage(
|
||||||
|
unitErrorUrl
|
||||||
|
? messages.errorDescriptionUnit
|
||||||
|
: messages.errorDescriptionNotUnit,
|
||||||
|
{ errorMessage },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cancelButtonText={
|
||||||
|
intl.formatMessage(unitErrorUrl ? messages.errorCancelButtonUnit : messages.errorCancelButtonNotUnit)
|
||||||
|
}
|
||||||
|
actionButtonText={
|
||||||
|
intl.formatMessage(unitErrorUrl ? messages.errorActionButtonUnit : messages.errorActionButtonNotUnit)
|
||||||
|
}
|
||||||
|
handleCancel={() => dispatch(updateIsErrorModalOpen(false))}
|
||||||
|
handleAction={unitErrorUrl ? handleUnitRedirect : handleRedirectCourseHome}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ExportModalError.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
courseId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
ExportModalError.defaultProps = {};
|
||||||
|
|
||||||
|
export default injectIntl(ExportModalError);
|
||||||
34
src/export-page/export-modal-error/messages.js
Normal file
34
src/export-page/export-modal-error/messages.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
errorTitle: {
|
||||||
|
id: 'course-authoring.export.modal.error.title',
|
||||||
|
defaultMessage: 'There has been an error while exporting.',
|
||||||
|
},
|
||||||
|
errorDescriptionNotUnit: {
|
||||||
|
id: 'course-authoring.export.modal.error.description.not.unit',
|
||||||
|
defaultMessage: 'Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again. The raw error message is: {errorMessage}',
|
||||||
|
},
|
||||||
|
errorDescriptionUnit: {
|
||||||
|
id: 'course-authoring.export.modal.error.description.unit',
|
||||||
|
defaultMessage: 'There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages. The raw error message is: {errorMessage}',
|
||||||
|
},
|
||||||
|
errorCancelButtonUnit: {
|
||||||
|
id: 'course-authoring.export.modal.error.button.cancel.unit',
|
||||||
|
defaultMessage: 'Return to export',
|
||||||
|
},
|
||||||
|
errorCancelButtonNotUnit: {
|
||||||
|
id: 'course-authoring.export.modal.error.button.cancel.not.unit',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
},
|
||||||
|
errorActionButtonNotUnit: {
|
||||||
|
id: 'course-authoring.export.modal.error.button.action.not.unit',
|
||||||
|
defaultMessage: 'Take me to the main course page',
|
||||||
|
},
|
||||||
|
errorActionButtonUnit: {
|
||||||
|
id: 'course-authoring.export.modal.error.button.action.unit',
|
||||||
|
defaultMessage: 'Correct failed component',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
49
src/export-page/export-sidebar/ExportSidebar.jsx
Normal file
49
src/export-page/export-sidebar/ExportSidebar.jsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
injectIntl,
|
||||||
|
intlShape,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Hyperlink } from '@edx/paragon';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||||
|
import { useHelpUrls } from '../../help-urls/hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const ExportSidebar = ({ intl, courseId }) => {
|
||||||
|
const { exportCourse: exportLearnMoreUrl } = useHelpUrls(['exportCourse']);
|
||||||
|
return (
|
||||||
|
<HelpSidebar courseId={courseId}>
|
||||||
|
<h4 className="help-sidebar-about-title">{intl.formatMessage(messages.title1)}</h4>
|
||||||
|
<p className="help-sidebar-about-descriptions">{intl.formatMessage(messages.description1, { studioShortName: getConfig().STUDIO_SHORT_NAME })}</p>
|
||||||
|
<hr />
|
||||||
|
<h4 className="help-sidebar-about-title">{intl.formatMessage(messages.exportedContent)}</h4>
|
||||||
|
<p className="help-sidebar-about-descriptions">{intl.formatMessage(messages.exportedContentHeading)}</p>
|
||||||
|
<ul className="px-3">
|
||||||
|
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content1)}</li>
|
||||||
|
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content2)}</li>
|
||||||
|
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content3)}</li>
|
||||||
|
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content4)}</li>
|
||||||
|
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content5)}</li>
|
||||||
|
</ul>
|
||||||
|
<p className="help-sidebar-about-descriptions">{intl.formatMessage(messages.notExportedContent)}</p>
|
||||||
|
<ul className="px-3">
|
||||||
|
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content6)}</li>
|
||||||
|
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content7)}</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<h4 className="help-sidebar-about-title">{intl.formatMessage(messages.openDownloadFile)}</h4>
|
||||||
|
<p className="help-sidebar-about-descriptions">{intl.formatMessage(messages.openDownloadFileDescription)}</p>
|
||||||
|
<hr />
|
||||||
|
<Hyperlink className="small" href={exportLearnMoreUrl} target="_blank" variant="outline-primary">{intl.formatMessage(messages.learnMoreButtonTitle)}</Hyperlink>
|
||||||
|
</HelpSidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ExportSidebar.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
courseId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ExportSidebar);
|
||||||
39
src/export-page/export-sidebar/ExportSidebar.test.jsx
Normal file
39
src/export-page/export-sidebar/ExportSidebar.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import initializeStore from '../../store';
|
||||||
|
import messages from './messages';
|
||||||
|
import ExportSidebar from './ExportSidebar';
|
||||||
|
|
||||||
|
const courseId = 'course-123';
|
||||||
|
let store;
|
||||||
|
|
||||||
|
const RootWrapper = () => (
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<ExportSidebar intl={{ formatMessage: jest.fn() }} courseId={courseId} />
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<ExportSidebar />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store = initializeStore();
|
||||||
|
});
|
||||||
|
it('render sidebar correctly', () => {
|
||||||
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
expect(getByText(messages.title1.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.exportedContentHeading.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
66
src/export-page/export-sidebar/messages.js
Normal file
66
src/export-page/export-sidebar/messages.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title1: {
|
||||||
|
id: 'course-authoring.export.sidebar.title1',
|
||||||
|
defaultMessage: 'Why export a course?',
|
||||||
|
},
|
||||||
|
description1: {
|
||||||
|
id: 'course-authoring.export.sidebar.description1',
|
||||||
|
defaultMessage: 'You may want to edit the XML in your course directly, outside of {studioShortName}. You may want to create a backup copy of your course. Or, you may want to create a copy of your course that you can later import into another course instance and customize.',
|
||||||
|
},
|
||||||
|
exportedContent: {
|
||||||
|
id: 'course-authoring.export.sidebar.exportedContent',
|
||||||
|
defaultMessage: 'What content is exported?',
|
||||||
|
},
|
||||||
|
exportedContentHeading: {
|
||||||
|
id: 'course-authoring.export.sidebar.exportedContentHeading',
|
||||||
|
defaultMessage: 'The following content is exported.',
|
||||||
|
},
|
||||||
|
content1: {
|
||||||
|
id: 'course-authoring.export.sidebar.content1',
|
||||||
|
defaultMessage: 'Course content and structure',
|
||||||
|
},
|
||||||
|
content2: {
|
||||||
|
id: 'course-authoring.export.sidebar.content2',
|
||||||
|
defaultMessage: 'Course dates',
|
||||||
|
},
|
||||||
|
content3: {
|
||||||
|
id: 'course-authoring.export.sidebar.content3',
|
||||||
|
defaultMessage: 'Grading policy',
|
||||||
|
},
|
||||||
|
content4: {
|
||||||
|
id: 'course-authoring.export.sidebar.content4',
|
||||||
|
defaultMessage: 'Any group configurations',
|
||||||
|
},
|
||||||
|
content5: {
|
||||||
|
id: 'course-authoring.export.sidebar.content5',
|
||||||
|
defaultMessage: 'Settings on the Advanced settings page, including MATLAB API keys and LTI passports',
|
||||||
|
},
|
||||||
|
notExportedContent: {
|
||||||
|
id: 'course-authoring.export.sidebar.notExportedContent',
|
||||||
|
defaultMessage: 'The following content is not exported.',
|
||||||
|
},
|
||||||
|
content6: {
|
||||||
|
id: 'course-authoring.export.sidebar.content6',
|
||||||
|
defaultMessage: 'Learner-specific content, such as learner grades and discussion forum data',
|
||||||
|
},
|
||||||
|
content7: {
|
||||||
|
id: 'course-authoring.export.sidebar.content7',
|
||||||
|
defaultMessage: 'The course team',
|
||||||
|
},
|
||||||
|
openDownloadFile: {
|
||||||
|
id: 'course-authoring.export.sidebar.openDownloadFile',
|
||||||
|
defaultMessage: 'Opening the downloaded file',
|
||||||
|
},
|
||||||
|
openDownloadFileDescription: {
|
||||||
|
id: 'course-authoring.export.sidebar.openDownloadFileDescription',
|
||||||
|
defaultMessage: 'Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.',
|
||||||
|
},
|
||||||
|
learnMoreButtonTitle: {
|
||||||
|
id: 'course-authoring.export.sidebar.learnMoreButtonTitle',
|
||||||
|
defaultMessage: 'Learn more about exporting a course',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
103
src/export-page/export-stepper/ExportStepper.jsx
Normal file
103
src/export-page/export-stepper/ExportStepper.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
FormattedDate,
|
||||||
|
injectIntl,
|
||||||
|
intlShape,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Button } from '@edx/paragon';
|
||||||
|
|
||||||
|
import CourseStepper from '../../generic/course-stepper';
|
||||||
|
import {
|
||||||
|
getCurrentStage, getDownloadPath, getError, getLoadingStatus, getSuccessDate,
|
||||||
|
} from '../data/selectors';
|
||||||
|
import { fetchExportStatus } from '../data/thunks';
|
||||||
|
import { EXPORT_STAGES } from '../data/constants';
|
||||||
|
import { RequestStatus } from '../../data/constants';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const ExportStepper = ({ intl, courseId }) => {
|
||||||
|
const currentStage = useSelector(getCurrentStage);
|
||||||
|
const downloadPath = useSelector(getDownloadPath);
|
||||||
|
const successDate = useSelector(getSuccessDate);
|
||||||
|
const loadingStatus = useSelector(getLoadingStatus);
|
||||||
|
const { msg: errorMessage } = useSelector(getError);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isStopFetching = currentStage === EXPORT_STAGES.SUCCESS
|
||||||
|
|| loadingStatus === RequestStatus.FAILED
|
||||||
|
|| errorMessage;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => {
|
||||||
|
if (isStopFetching) {
|
||||||
|
clearInterval(id);
|
||||||
|
} else {
|
||||||
|
dispatch(fetchExportStatus(courseId));
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
let successTitle = intl.formatMessage(messages.stepperSuccessTitle);
|
||||||
|
const localizedSuccessDate = successDate ? (
|
||||||
|
<FormattedDate
|
||||||
|
value={successDate}
|
||||||
|
year="2-digit"
|
||||||
|
month="2-digit"
|
||||||
|
day="2-digit"
|
||||||
|
hour="numeric"
|
||||||
|
minute="numeric"
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
if (localizedSuccessDate && currentStage === EXPORT_STAGES.SUCCESS) {
|
||||||
|
const successWithDate = (
|
||||||
|
<>
|
||||||
|
{successTitle} ({localizedSuccessDate})
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
successTitle = successWithDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: intl.formatMessage(messages.stepperPreparingTitle),
|
||||||
|
description: intl.formatMessage(messages.stepperPreparingDescription),
|
||||||
|
key: EXPORT_STAGES.PREPARING,
|
||||||
|
}, {
|
||||||
|
title: intl.formatMessage(messages.stepperExportingTitle),
|
||||||
|
description: intl.formatMessage(messages.stepperExportingDescription),
|
||||||
|
key: EXPORT_STAGES.EXPORTING,
|
||||||
|
}, {
|
||||||
|
title: intl.formatMessage(messages.stepperCompressingTitle),
|
||||||
|
description: intl.formatMessage(messages.stepperCompressingDescription),
|
||||||
|
key: EXPORT_STAGES.COMPRESSING,
|
||||||
|
}, {
|
||||||
|
title: successTitle,
|
||||||
|
description: intl.formatMessage(messages.stepperSuccessDescription),
|
||||||
|
key: EXPORT_STAGES.SUCCESS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="mt-4">{intl.formatMessage(messages.stepperHeaderTitle)}</h3>
|
||||||
|
<CourseStepper
|
||||||
|
courseId={courseId}
|
||||||
|
steps={steps}
|
||||||
|
activeKey={currentStage}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
hasError={!!errorMessage}
|
||||||
|
/>
|
||||||
|
{downloadPath && currentStage === EXPORT_STAGES.SUCCESS && <Button className="ml-5.5 mt-n2.5" href={downloadPath} download>{intl.formatMessage(messages.downloadCourseButtonTitle)}</Button>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ExportStepper.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
courseId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ExportStepper);
|
||||||
38
src/export-page/export-stepper/ExportStepper.test.jsx
Normal file
38
src/export-page/export-stepper/ExportStepper.test.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import initializeStore from '../../store';
|
||||||
|
import messages from './messages';
|
||||||
|
import ExportStepper from './ExportStepper';
|
||||||
|
|
||||||
|
const courseId = 'course-123';
|
||||||
|
let store;
|
||||||
|
|
||||||
|
const RootWrapper = () => (
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<ExportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} />
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<ExportStepper />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store = initializeStore();
|
||||||
|
});
|
||||||
|
it('render stepper correctly', () => {
|
||||||
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
46
src/export-page/export-stepper/messages.js
Normal file
46
src/export-page/export-stepper/messages.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
stepperPreparingTitle: {
|
||||||
|
id: 'course-authoring.export.stepper.title.preparing',
|
||||||
|
defaultMessage: 'Preparing',
|
||||||
|
},
|
||||||
|
stepperExportingTitle: {
|
||||||
|
id: 'course-authoring.export.stepper.title.exporting',
|
||||||
|
defaultMessage: 'Exporting',
|
||||||
|
},
|
||||||
|
stepperCompressingTitle: {
|
||||||
|
id: 'course-authoring.export.stepper.title.compressing',
|
||||||
|
defaultMessage: 'Compressing',
|
||||||
|
},
|
||||||
|
stepperSuccessTitle: {
|
||||||
|
id: 'course-authoring.export.stepper.title.success',
|
||||||
|
defaultMessage: 'Success',
|
||||||
|
},
|
||||||
|
stepperPreparingDescription: {
|
||||||
|
id: 'course-authoring.export.stepper.description.preparing',
|
||||||
|
defaultMessage: 'Preparing to start the export',
|
||||||
|
},
|
||||||
|
stepperExportingDescription: {
|
||||||
|
id: 'course-authoring.export.stepper.description.exporting',
|
||||||
|
defaultMessage: 'Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete)',
|
||||||
|
},
|
||||||
|
stepperCompressingDescription: {
|
||||||
|
id: 'course-authoring.export.stepper.description.compressing',
|
||||||
|
defaultMessage: 'Compressing the exported data and preparing it for download',
|
||||||
|
},
|
||||||
|
stepperSuccessDescription: {
|
||||||
|
id: 'course-authoring.export.stepper.description.success',
|
||||||
|
defaultMessage: 'Your exported course can now be downloaded',
|
||||||
|
},
|
||||||
|
downloadCourseButtonTitle: {
|
||||||
|
id: 'course-authoring.export.stepper.download.button.title',
|
||||||
|
defaultMessage: 'Download exported course',
|
||||||
|
},
|
||||||
|
stepperHeaderTitle: {
|
||||||
|
id: 'course-authoring.export.stepper.header.title',
|
||||||
|
defaultMessage: 'Course export status',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
34
src/export-page/messages.js
Normal file
34
src/export-page/messages.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
pageTitle: {
|
||||||
|
id: 'course-authoring.export.page.title',
|
||||||
|
defaultMessage: '{headingTitle} | {courseName} | {siteName}',
|
||||||
|
},
|
||||||
|
headingTitle: {
|
||||||
|
id: 'course-authoring.export.heading.title',
|
||||||
|
defaultMessage: 'Course export',
|
||||||
|
},
|
||||||
|
headingSubtitle: {
|
||||||
|
id: 'course-authoring.export.heading.subtitle',
|
||||||
|
defaultMessage: 'Tools',
|
||||||
|
},
|
||||||
|
description1: {
|
||||||
|
id: 'course-authoring.export.description1',
|
||||||
|
defaultMessage: 'You can export courses and edit them outside of {studioShortName}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you\'ve exported.',
|
||||||
|
},
|
||||||
|
description2: {
|
||||||
|
id: 'course-authoring.export.description2',
|
||||||
|
defaultMessage: 'Caution: When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.',
|
||||||
|
},
|
||||||
|
titleUnderButton: {
|
||||||
|
id: 'course-authoring.export.title-under-button',
|
||||||
|
defaultMessage: 'Export my course content',
|
||||||
|
},
|
||||||
|
buttonTitle: {
|
||||||
|
id: 'course-authoring.export.button.title',
|
||||||
|
defaultMessage: 'Export course content',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
29
src/export-page/utils.js
Normal file
29
src/export-page/utils.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { TIME_FORMAT } from '../constants';
|
||||||
|
import { LAST_EXPORT_COOKIE_NAME, SUCCESS_DATE_FORMAT } from './data/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an export-related cookie with the provided information.
|
||||||
|
*
|
||||||
|
* @param {Date} date - Date of export.
|
||||||
|
* @param {boolean} completed - Indicates if export was completed successfully.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export const setExportCookie = (date, completed) => {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
cookies.set(LAST_EXPORT_COOKIE_NAME, { date, completed }, { path: window.location.pathname });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a Unix timestamp as a formatted success date string.
|
||||||
|
*
|
||||||
|
* @param {number} unixDate - Unix timestamp to be formatted.
|
||||||
|
* @returns {string|null} Formatted success date string, including date and time in UTC, or null if the input is falsy.
|
||||||
|
*/
|
||||||
|
export const getFormattedSuccessDate = (unixDate) => {
|
||||||
|
const formattedDate = moment(unixDate).utc().format(SUCCESS_DATE_FORMAT);
|
||||||
|
const formattedTime = moment(unixDate).utc().format(TIME_FORMAT);
|
||||||
|
return unixDate ? ` (${formattedDate} at ${formattedTime} UTC)` : null;
|
||||||
|
};
|
||||||
51
src/export-page/utils.test.js
Normal file
51
src/export-page/utils.test.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { LAST_EXPORT_COOKIE_NAME } from './data/constants';
|
||||||
|
import { setExportCookie, getFormattedSuccessDate } from './utils';
|
||||||
|
|
||||||
|
global.window = Object.create(window);
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: {
|
||||||
|
pathname: '/some-path',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setExportCookie', () => {
|
||||||
|
it('should set the export cookie with the provided date and completed status', () => {
|
||||||
|
const cookiesSetMock = jest.spyOn(Cookies.prototype, 'set');
|
||||||
|
const date = '2023-07-24';
|
||||||
|
const completed = true;
|
||||||
|
setExportCookie(date, completed);
|
||||||
|
|
||||||
|
expect(cookiesSetMock).toHaveBeenCalledWith(
|
||||||
|
LAST_EXPORT_COOKIE_NAME,
|
||||||
|
{ date, completed },
|
||||||
|
{ path: '/some-path' },
|
||||||
|
);
|
||||||
|
|
||||||
|
cookiesSetMock.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFormattedSuccessDate', () => {
|
||||||
|
it('should return formatted success date with valid input', () => {
|
||||||
|
const mockCurrentUtcDate = moment.utc('2023-07-24T12:34:56');
|
||||||
|
const momentMock = jest.spyOn(moment, 'utc').mockReturnValue(mockCurrentUtcDate);
|
||||||
|
|
||||||
|
const unixDate = 1679787000;
|
||||||
|
|
||||||
|
const expectedFormattedDate = ' (01/20/1970 at 10:36 UTC)';
|
||||||
|
|
||||||
|
const result = getFormattedSuccessDate(unixDate);
|
||||||
|
expect(result).toBe(expectedFormattedDate);
|
||||||
|
|
||||||
|
momentMock.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null with null input', () => {
|
||||||
|
const unixDate = null;
|
||||||
|
const result = getFormattedSuccessDate(unixDate);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export const fileInput = ({
|
export const useFileInput = ({
|
||||||
onAddFile,
|
onAddFile,
|
||||||
setSelectedRows,
|
setSelectedRows,
|
||||||
setAddOpen,
|
setAddOpen,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const ref = React.useRef();
|
const ref = React.useRef();
|
||||||
const click = () => ref.current.click();
|
const click = () => ref.current.click();
|
||||||
const addFile = (e) => {
|
const addFile = (e) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const FileMenu = ({
|
|||||||
externalUrl,
|
externalUrl,
|
||||||
handleLock,
|
handleLock,
|
||||||
locked,
|
locked,
|
||||||
|
onDownload,
|
||||||
openAssetInfo,
|
openAssetInfo,
|
||||||
openDeleteConfirmation,
|
openDeleteConfirmation,
|
||||||
portableUrl,
|
portableUrl,
|
||||||
@@ -40,7 +41,7 @@ const FileMenu = ({
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(messages.copyWebUrlTitle)}
|
{intl.formatMessage(messages.copyWebUrlTitle)}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item href={externalUrl} target="_blank" download>
|
<Dropdown.Item onClick={onDownload}>
|
||||||
{intl.formatMessage(messages.downloadTitle)}
|
{intl.formatMessage(messages.downloadTitle)}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item onClick={handleLock}>
|
<Dropdown.Item onClick={handleLock}>
|
||||||
@@ -64,6 +65,7 @@ FileMenu.propTypes = {
|
|||||||
externalUrl: PropTypes.string.isRequired,
|
externalUrl: PropTypes.string.isRequired,
|
||||||
handleLock: PropTypes.func.isRequired,
|
handleLock: PropTypes.func.isRequired,
|
||||||
locked: PropTypes.bool.isRequired,
|
locked: PropTypes.bool.isRequired,
|
||||||
|
onDownload: PropTypes.func.isRequired,
|
||||||
openAssetInfo: PropTypes.func.isRequired,
|
openAssetInfo: PropTypes.func.isRequired,
|
||||||
openDeleteConfirmation: PropTypes.func.isRequired,
|
openDeleteConfirmation: PropTypes.func.isRequired,
|
||||||
portableUrl: PropTypes.string.isRequired,
|
portableUrl: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
@@ -17,20 +17,22 @@ import {
|
|||||||
import Placeholder, { ErrorAlert } from '@edx/frontend-lib-content-components';
|
import Placeholder, { ErrorAlert } from '@edx/frontend-lib-content-components';
|
||||||
|
|
||||||
import { RequestStatus } from '../data/constants';
|
import { RequestStatus } from '../data/constants';
|
||||||
import { useModels } from '../generic/model-store';
|
import { useModels, useModel } from '../generic/model-store';
|
||||||
import {
|
import {
|
||||||
addAssetFile,
|
addAssetFile,
|
||||||
deleteAssetFile,
|
deleteAssetFile,
|
||||||
fetchAssets,
|
fetchAssets,
|
||||||
|
resetErrors,
|
||||||
getUsagePaths,
|
getUsagePaths,
|
||||||
updateAssetLock,
|
updateAssetLock,
|
||||||
updateAssetOrder,
|
updateAssetOrder,
|
||||||
|
fetchAssetDownload,
|
||||||
} from './data/thunks';
|
} from './data/thunks';
|
||||||
import { sortFiles } from './data/utils';
|
import { sortFiles } from './data/utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
import FileInfo from './FileInfo';
|
import FileInfo from './FileInfo';
|
||||||
import FileInput, { fileInput } from './FileInput';
|
import FileInput, { useFileInput } from './FileInput';
|
||||||
import FilesAndUploadsProvider from './FilesAndUploadsProvider';
|
import FilesAndUploadsProvider from './FilesAndUploadsProvider';
|
||||||
import {
|
import {
|
||||||
GalleryCard,
|
GalleryCard,
|
||||||
@@ -38,6 +40,8 @@ import {
|
|||||||
TableActions,
|
TableActions,
|
||||||
} from './table-components';
|
} from './table-components';
|
||||||
import ApiStatusToast from './ApiStatusToast';
|
import ApiStatusToast from './ApiStatusToast';
|
||||||
|
import { clearErrors } from './data/slice';
|
||||||
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
|
||||||
const FilesAndUploads = ({
|
const FilesAndUploads = ({
|
||||||
courseId,
|
courseId,
|
||||||
@@ -59,6 +63,9 @@ const FilesAndUploads = ({
|
|||||||
const [selectedRows, setSelectedRows] = useState([]);
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
const [isDeleteConfirmationOpen, openDeleteConfirmation, closeDeleteConfirmation] = useToggle(false);
|
const [isDeleteConfirmationOpen, openDeleteConfirmation, closeDeleteConfirmation] = useToggle(false);
|
||||||
|
|
||||||
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchAssets(courseId));
|
dispatch(fetchAssets(courseId));
|
||||||
}, [courseId]);
|
}, [courseId]);
|
||||||
@@ -72,7 +79,7 @@ const FilesAndUploads = ({
|
|||||||
usageStatus: usagePathStatus,
|
usageStatus: usagePathStatus,
|
||||||
errors: errorMessages,
|
errors: errorMessages,
|
||||||
} = useSelector(state => state.assets);
|
} = useSelector(state => state.assets);
|
||||||
const fileInputControl = fileInput({
|
const fileInputControl = useFileInput({
|
||||||
onAddFile: (file) => dispatch(addAssetFile(courseId, file, totalCount)),
|
onAddFile: (file) => dispatch(addAssetFile(courseId, file, totalCount)),
|
||||||
setSelectedRows,
|
setSelectedRows,
|
||||||
setAddOpen,
|
setAddOpen,
|
||||||
@@ -95,25 +102,18 @@ const FilesAndUploads = ({
|
|||||||
const handleBulkDelete = () => {
|
const handleBulkDelete = () => {
|
||||||
closeDeleteConfirmation();
|
closeDeleteConfirmation();
|
||||||
setDeleteOpen();
|
setDeleteOpen();
|
||||||
|
dispatch(resetErrors({ errorType: 'delete' }));
|
||||||
const assetIdsToDelete = selectedRows.map(row => row.original.id);
|
const assetIdsToDelete = selectedRows.map(row => row.original.id);
|
||||||
assetIdsToDelete.forEach(id => dispatch(deleteAssetFile(courseId, id, totalCount)));
|
assetIdsToDelete.forEach(id => dispatch(deleteAssetFile(courseId, id, totalCount)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkDownload = (selectedFlatRows) => {
|
const handleBulkDownload = useCallback(async (selectedFlatRows) => {
|
||||||
selectedFlatRows.forEach(row => {
|
dispatch(resetErrors({ errorType: 'download' }));
|
||||||
const { externalUrl } = row.original;
|
dispatch(fetchAssetDownload({ selectedRows: selectedFlatRows, courseId }));
|
||||||
const link = document.createElement('a');
|
}, []);
|
||||||
link.target = '_blank';
|
|
||||||
link.download = true;
|
|
||||||
link.href = externalUrl;
|
|
||||||
link.click();
|
|
||||||
});
|
|
||||||
/* ********** TODO ***********
|
|
||||||
* implement a zip file function when there are multiple files
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLockedAsset = (assetId, locked) => {
|
const handleLockedAsset = (assetId, locked) => {
|
||||||
|
dispatch(clearErrors({ errorType: 'lock' }));
|
||||||
dispatch(updateAssetLock({ courseId, assetId, locked }));
|
dispatch(updateAssetLock({ courseId, assetId, locked }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,6 +123,7 @@ const FilesAndUploads = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenAssetInfo = (original) => {
|
const handleOpenAssetInfo = (original) => {
|
||||||
|
dispatch(resetErrors({ errorType: 'usageMetrics' }));
|
||||||
setSelectedRows([{ original }]);
|
setSelectedRows([{ original }]);
|
||||||
dispatch(getUsagePaths({ asset: original, courseId, setSelectedRows }));
|
dispatch(getUsagePaths({ asset: original, courseId, setSelectedRows }));
|
||||||
openAssetInfo();
|
openAssetInfo();
|
||||||
@@ -146,6 +147,7 @@ const FilesAndUploads = ({
|
|||||||
<GalleryCard
|
<GalleryCard
|
||||||
{...{
|
{...{
|
||||||
handleLockedAsset,
|
handleLockedAsset,
|
||||||
|
handleBulkDownload,
|
||||||
handleOpenDeleteConfirmation,
|
handleOpenDeleteConfirmation,
|
||||||
handleOpenAssetInfo,
|
handleOpenAssetInfo,
|
||||||
className,
|
className,
|
||||||
@@ -158,6 +160,7 @@ const FilesAndUploads = ({
|
|||||||
<ListCard
|
<ListCard
|
||||||
{...{
|
{...{
|
||||||
handleLockedAsset,
|
handleLockedAsset,
|
||||||
|
handleBulkDownload,
|
||||||
handleOpenDeleteConfirmation,
|
handleOpenDeleteConfirmation,
|
||||||
handleOpenAssetInfo,
|
handleOpenAssetInfo,
|
||||||
className,
|
className,
|
||||||
@@ -183,8 +186,8 @@ const FilesAndUploads = ({
|
|||||||
isError={addAssetStatus === RequestStatus.FAILED}
|
isError={addAssetStatus === RequestStatus.FAILED}
|
||||||
>
|
>
|
||||||
<ul className="p-0">
|
<ul className="p-0">
|
||||||
{errorMessages.upload.map(message => (
|
{errorMessages.add.map(message => (
|
||||||
<li style={{ listStyle: 'none' }}>
|
<li key={`add-error-${message}`} style={{ listStyle: 'none' }}>
|
||||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -196,7 +199,7 @@ const FilesAndUploads = ({
|
|||||||
>
|
>
|
||||||
<ul className="p-0">
|
<ul className="p-0">
|
||||||
{errorMessages.delete.map(message => (
|
{errorMessages.delete.map(message => (
|
||||||
<li style={{ listStyle: 'none' }}>
|
<li key={`delete-error-${message}`} style={{ listStyle: 'none' }}>
|
||||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -208,12 +211,16 @@ const FilesAndUploads = ({
|
|||||||
>
|
>
|
||||||
<ul className="p-0">
|
<ul className="p-0">
|
||||||
{errorMessages.lock.map(message => (
|
{errorMessages.lock.map(message => (
|
||||||
<li style={{ listStyle: 'none' }}>
|
<li key={`lock-error-${message}`} style={{ listStyle: 'none' }}>
|
||||||
|
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{errorMessages.download.map(message => (
|
||||||
|
<li key={`download-error-${message}`} style={{ listStyle: 'none' }}>
|
||||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</ErrorAlert>
|
</ErrorAlert>
|
||||||
<div className="h2">
|
<div className="h2">
|
||||||
<FormattedMessage {...messages.heading} />
|
<FormattedMessage {...messages.heading} />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
import { initializeMockApp } from '@edx/frontend-platform';
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
@@ -42,6 +43,7 @@ let axiosMock;
|
|||||||
let store;
|
let store;
|
||||||
let file;
|
let file;
|
||||||
ReactDOM.createPortal = jest.fn(node => node);
|
ReactDOM.createPortal = jest.fn(node => node);
|
||||||
|
jest.mock('file-saver');
|
||||||
|
|
||||||
const renderComponent = () => {
|
const renderComponent = () => {
|
||||||
render(
|
render(
|
||||||
@@ -89,22 +91,27 @@ describe('FilesAndUploads', () => {
|
|||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return placeholder component', async () => {
|
it('should return placeholder component', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.DENIED);
|
await mockStore(RequestStatus.DENIED);
|
||||||
expect(screen.getByTestId('under-construction-placeholder')).toBeVisible();
|
expect(screen.getByTestId('under-construction-placeholder')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have Files and uploads title', async () => {
|
it('should have Files and uploads title', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByText('Files and uploads')).toBeVisible();
|
expect(screen.getByText('Files and uploads')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render dropzone', async () => {
|
it('should render dropzone', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('files-dropzone')).toBeVisible();
|
expect(screen.getByTestId('files-dropzone')).toBeVisible();
|
||||||
|
|
||||||
expect(screen.queryByTestId('files-data-table')).toBeNull();
|
expect(screen.queryByTestId('files-data-table')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should upload a single file', async () => {
|
it('should upload a single file', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
||||||
@@ -119,10 +126,13 @@ describe('FilesAndUploads', () => {
|
|||||||
});
|
});
|
||||||
const addStatus = store.getState().assets.addingStatus;
|
const addStatus = store.getState().assets.addingStatus;
|
||||||
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
|
|
||||||
expect(screen.queryByTestId('files-dropzone')).toBeNull();
|
expect(screen.queryByTestId('files-dropzone')).toBeNull();
|
||||||
|
|
||||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('valid assets', () => {
|
describe('valid assets', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
@@ -137,27 +147,39 @@ describe('FilesAndUploads', () => {
|
|||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
saveAs.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
describe('table view', () => {
|
describe('table view', () => {
|
||||||
it('should render table with gallery card', async () => {
|
it('should render table with gallery card', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
||||||
|
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should switch table to list view', async () => {
|
it('should switch table to list view', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
||||||
|
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
|
|
||||||
expect(screen.queryByTestId('list-card-mOckID1')).toBeNull();
|
expect(screen.queryByTestId('list-card-mOckID1')).toBeNull();
|
||||||
|
|
||||||
const listButton = screen.getByLabelText('List');
|
const listButton = screen.getByLabelText('List');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(listButton);
|
fireEvent.click(listButton);
|
||||||
});
|
});
|
||||||
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
||||||
|
|
||||||
expect(screen.getByTestId('list-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('list-card-mOckID1')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('table actions', () => {
|
describe('table actions', () => {
|
||||||
it('should upload a single file', async () => {
|
it('should upload a single file', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
@@ -171,17 +193,21 @@ describe('FilesAndUploads', () => {
|
|||||||
const addStatus = store.getState().assets.addingStatus;
|
const addStatus = store.getState().assets.addingStatus;
|
||||||
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have disabled action buttons', async () => {
|
it('should have disabled action buttons', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||||
expect(actionsButton).toBeVisible();
|
expect(actionsButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
fireEvent.click(actionsButton);
|
fireEvent.click(actionsButton);
|
||||||
});
|
});
|
||||||
expect(screen.getByText(messages.downloadTitle.defaultMessage).closest('a')).toHaveClass('disabled');
|
expect(screen.getByText(messages.downloadTitle.defaultMessage).closest('a')).toHaveClass('disabled');
|
||||||
|
|
||||||
expect(screen.getByText(messages.deleteTitle.defaultMessage).closest('a')).toHaveClass('disabled');
|
expect(screen.getByText(messages.deleteTitle.defaultMessage).closest('a')).toHaveClass('disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delete button should be enabled and delete selected file', async () => {
|
it('delete button should be enabled and delete selected file', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
@@ -189,59 +215,113 @@ describe('FilesAndUploads', () => {
|
|||||||
fireEvent.click(selectCardButton);
|
fireEvent.click(selectCardButton);
|
||||||
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||||
expect(actionsButton).toBeVisible();
|
expect(actionsButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
fireEvent.click(actionsButton);
|
fireEvent.click(actionsButton);
|
||||||
});
|
});
|
||||||
const deleteButton = screen.getByText(messages.deleteTitle.defaultMessage).closest('a');
|
const deleteButton = screen.getByText(messages.deleteTitle.defaultMessage).closest('a');
|
||||||
expect(deleteButton).not.toHaveClass('disabled');
|
expect(deleteButton).not.toHaveClass('disabled');
|
||||||
|
|
||||||
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
|
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
fireEvent.click(deleteButton);
|
fireEvent.click(deleteButton);
|
||||||
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
||||||
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
||||||
|
|
||||||
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
||||||
});
|
});
|
||||||
const deleteStatus = store.getState().assets.deletingStatus;
|
const deleteStatus = store.getState().assets.deletingStatus;
|
||||||
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
|
|
||||||
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('download button should be enabled and download single selected file', async () => {
|
||||||
|
renderComponent();
|
||||||
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
|
const selectCardButton = screen.getAllByTestId('datatable-select-column-checkbox-cell')[0];
|
||||||
|
fireEvent.click(selectCardButton);
|
||||||
|
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||||
|
expect(actionsButton).toBeVisible();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
fireEvent.click(actionsButton);
|
||||||
|
});
|
||||||
|
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||||
|
expect(downloadButton).not.toHaveClass('disabled');
|
||||||
|
|
||||||
|
fireEvent.click(downloadButton);
|
||||||
|
expect(saveAs).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('download button should be enabled and download multiple selected files', async () => {
|
||||||
|
renderComponent();
|
||||||
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
|
const selectCardButtons = screen.getAllByTestId('datatable-select-column-checkbox-cell');
|
||||||
|
fireEvent.click(selectCardButtons[0]);
|
||||||
|
fireEvent.click(selectCardButtons[1]);
|
||||||
|
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||||
|
expect(actionsButton).toBeVisible();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
fireEvent.click(actionsButton);
|
||||||
|
});
|
||||||
|
const mockResponseData = { ok: true, blob: () => 'Data' };
|
||||||
|
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||||
|
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||||
|
expect(downloadButton).not.toHaveClass('disabled');
|
||||||
|
|
||||||
|
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||||
|
fireEvent.click(downloadButton);
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('sort button should be enabled and sort files by name', async () => {
|
it('sort button should be enabled and sort files by name', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
|
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
|
||||||
expect(sortsButton).toBeVisible();
|
expect(sortsButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
fireEvent.click(sortsButton);
|
fireEvent.click(sortsButton);
|
||||||
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
|
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortNameAscendingButton = screen.getByText(messages.sortByNameAscending.defaultMessage);
|
const sortNameAscendingButton = screen.getByText(messages.sortByNameAscending.defaultMessage);
|
||||||
fireEvent.click(sortNameAscendingButton);
|
fireEvent.click(sortNameAscendingButton);
|
||||||
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
|
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
|
||||||
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
|
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sort button should be enabled and sort files by file size', async () => {
|
it('sort button should be enabled and sort files by file size', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
|
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
|
||||||
expect(sortsButton).toBeVisible();
|
expect(sortsButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
fireEvent.click(sortsButton);
|
fireEvent.click(sortsButton);
|
||||||
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
|
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortBySizeDescendingButton = screen.getByText(messages.sortBySizeDescending.defaultMessage);
|
const sortBySizeDescendingButton = screen.getByText(messages.sortBySizeDescending.defaultMessage);
|
||||||
fireEvent.click(sortBySizeDescendingButton);
|
fireEvent.click(sortBySizeDescendingButton);
|
||||||
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
|
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
|
||||||
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
|
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('card menu actions', () => {
|
describe('card menu actions', () => {
|
||||||
it('should open asset info', async () => {
|
it('should open asset info', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
|
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID1/usage`).reply(201, { usageLocations: ['subsection - unit / block'] });
|
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID1/usage`).reply(201, { usageLocations: ['subsection - unit / block'] });
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
@@ -253,16 +333,19 @@ describe('FilesAndUploads', () => {
|
|||||||
}), store.dispatch);
|
}), store.dispatch);
|
||||||
expect(screen.getAllByLabelText('mOckID1')[0]).toBeVisible();
|
expect(screen.getAllByLabelText('mOckID1')[0]).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { usageStatus } = store.getState().assets;
|
const { usageStatus } = store.getState().assets;
|
||||||
expect(usageStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(usageStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByText('subsection - unit / block')).toBeVisible();
|
expect(screen.getByText('subsection - unit / block')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open asset info and handle lock checkbox', async () => {
|
it('should open asset info and handle lock checkbox', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID1`).reply(201, { locked: false });
|
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID1`).reply(201, { locked: false });
|
||||||
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID1/usage`).reply(201, { usageLocations: [] });
|
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID1/usage`).reply(201, { usageLocations: [] });
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -274,6 +357,7 @@ describe('FilesAndUploads', () => {
|
|||||||
setSelectedRows: jest.fn(),
|
setSelectedRows: jest.fn(),
|
||||||
}), store.dispatch);
|
}), store.dispatch);
|
||||||
expect(screen.getAllByLabelText('mOckID1')[0]).toBeVisible();
|
expect(screen.getAllByLabelText('mOckID1')[0]).toBeVisible();
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText('Checkbox'));
|
fireEvent.click(screen.getByLabelText('Checkbox'));
|
||||||
executeThunk(updateAssetLock({
|
executeThunk(updateAssetLock({
|
||||||
courseId,
|
courseId,
|
||||||
@@ -282,15 +366,19 @@ describe('FilesAndUploads', () => {
|
|||||||
}), store.dispatch);
|
}), store.dispatch);
|
||||||
});
|
});
|
||||||
expect(screen.getByText(messages.usageNotInUseMessage.defaultMessage)).toBeVisible();
|
expect(screen.getByText(messages.usageNotInUseMessage.defaultMessage)).toBeVisible();
|
||||||
|
|
||||||
const updateStatus = store.getState().assets.updatingStatus;
|
const updateStatus = store.getState().assets.updatingStatus;
|
||||||
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unlock asset', async () => {
|
it('should unlock asset', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
|
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID1`).reply(201, { locked: false });
|
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID1`).reply(201, { locked: false });
|
||||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
@@ -304,12 +392,15 @@ describe('FilesAndUploads', () => {
|
|||||||
const updateStatus = store.getState().assets.updatingStatus;
|
const updateStatus = store.getState().assets.updatingStatus;
|
||||||
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should lock asset', async () => {
|
it('should lock asset', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
||||||
|
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID3`).reply(201, { locked: true });
|
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID3`).reply(201, { locked: true });
|
||||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
@@ -323,26 +414,48 @@ describe('FilesAndUploads', () => {
|
|||||||
const updateStatus = store.getState().assets.updatingStatus;
|
const updateStatus = store.getState().assets.updatingStatus;
|
||||||
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('download button should download file', async () => {
|
||||||
|
renderComponent();
|
||||||
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
|
|
||||||
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||||
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
|
fireEvent.click(screen.getByText('Download'));
|
||||||
|
});
|
||||||
|
expect(saveAs).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('delete button should delete file', async () => {
|
it('delete button should delete file', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
|
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
|
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
|
||||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
|
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
|
||||||
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
||||||
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
||||||
|
|
||||||
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
||||||
});
|
});
|
||||||
const deleteStatus = store.getState().assets.deletingStatus;
|
const deleteStatus = store.getState().assets.deletingStatus;
|
||||||
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||||
|
|
||||||
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('api errors', () => {
|
describe('api errors', () => {
|
||||||
it('invalid file size should show error', async () => {
|
it('invalid file size should show error', async () => {
|
||||||
const errorMessage = 'File download.png exceeds maximum size of 20 MB.';
|
const errorMessage = 'File download.png exceeds maximum size of 20 MB.';
|
||||||
@@ -356,8 +469,10 @@ describe('FilesAndUploads', () => {
|
|||||||
});
|
});
|
||||||
const addStatus = store.getState().assets.addingStatus;
|
const addStatus = store.getState().assets.addingStatus;
|
||||||
expect(addStatus).toEqual(RequestStatus.FAILED);
|
expect(addStatus).toEqual(RequestStatus.FAILED);
|
||||||
|
|
||||||
expect(screen.getByText('Error')).toBeVisible();
|
expect(screen.getByText('Error')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('404 upload should show error', async () => {
|
it('404 upload should show error', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
@@ -369,35 +484,45 @@ describe('FilesAndUploads', () => {
|
|||||||
});
|
});
|
||||||
const addStatus = store.getState().assets.addingStatus;
|
const addStatus = store.getState().assets.addingStatus;
|
||||||
expect(addStatus).toEqual(RequestStatus.FAILED);
|
expect(addStatus).toEqual(RequestStatus.FAILED);
|
||||||
|
|
||||||
expect(screen.getByText('Error')).toBeVisible();
|
expect(screen.getByText('Error')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('404 delete should show error', async () => {
|
it('404 delete should show error', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
|
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
|
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(404);
|
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(404);
|
||||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
|
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
|
||||||
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
||||||
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
||||||
|
|
||||||
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
||||||
});
|
});
|
||||||
const deleteStatus = store.getState().assets.deletingStatus;
|
const deleteStatus = store.getState().assets.deletingStatus;
|
||||||
expect(deleteStatus).toEqual(RequestStatus.FAILED);
|
expect(deleteStatus).toEqual(RequestStatus.FAILED);
|
||||||
|
|
||||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||||
|
|
||||||
expect(screen.getByText('Error')).toBeVisible();
|
expect(screen.getByText('Error')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('404 usage path fetch should show error', async () => {
|
it('404 usage path fetch should show error', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
||||||
|
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID3/usage`).reply(404);
|
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID3/usage`).reply(404);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
@@ -411,12 +536,15 @@ describe('FilesAndUploads', () => {
|
|||||||
const { usageStatus } = store.getState().assets;
|
const { usageStatus } = store.getState().assets;
|
||||||
expect(usageStatus).toEqual(RequestStatus.FAILED);
|
expect(usageStatus).toEqual(RequestStatus.FAILED);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('404 lock update should show error', async () => {
|
it('404 lock update should show error', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await mockStore(RequestStatus.SUCCESSFUL);
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
||||||
|
|
||||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
||||||
expect(assetMenuButton).toBeVisible();
|
expect(assetMenuButton).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID3`).reply(404);
|
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID3`).reply(404);
|
||||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||||
@@ -429,6 +557,36 @@ describe('FilesAndUploads', () => {
|
|||||||
});
|
});
|
||||||
const updateStatus = store.getState().assets.updatingStatus;
|
const updateStatus = store.getState().assets.updatingStatus;
|
||||||
expect(updateStatus).toEqual(RequestStatus.FAILED);
|
expect(updateStatus).toEqual(RequestStatus.FAILED);
|
||||||
|
|
||||||
|
expect(screen.getByText('Error')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple asset file fetch failure should show error', async () => {
|
||||||
|
renderComponent();
|
||||||
|
await mockStore(RequestStatus.SUCCESSFUL);
|
||||||
|
const selectCardButtons = screen.getAllByTestId('datatable-select-column-checkbox-cell');
|
||||||
|
fireEvent.click(selectCardButtons[0]);
|
||||||
|
fireEvent.click(selectCardButtons[1]);
|
||||||
|
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||||
|
expect(actionsButton).toBeVisible();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
fireEvent.click(actionsButton);
|
||||||
|
});
|
||||||
|
const mockResponseData = { ok: false };
|
||||||
|
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||||
|
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||||
|
expect(downloadButton).not.toHaveClass('disabled');
|
||||||
|
|
||||||
|
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||||
|
await waitFor(() => {
|
||||||
|
fireEvent.click(downloadButton);
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateStatus = store.getState().assets.updatingStatus;
|
||||||
|
expect(updateStatus).toEqual(RequestStatus.FAILED);
|
||||||
|
|
||||||
expect(screen.getByText('Error')).toBeVisible();
|
expect(screen.getByText('Error')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ const UsageMetricsMessage = ({
|
|||||||
<FormattedMessage {...messages.usageNotInUseMessage} />
|
<FormattedMessage {...messages.usageNotInUseMessage} />
|
||||||
) : (
|
) : (
|
||||||
<ul className="p-0">
|
<ul className="p-0">
|
||||||
{usageLocations.map((location) => (<li style={{ listStyle: 'none' }}>{location}</li>))}
|
{usageLocations.map(location => (
|
||||||
|
<li key={`usage-location-${location}`} style={{ listStyle: 'none' }}>
|
||||||
|
{location}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
} else if (usagePathStatus === RequestStatus.FAILED) {
|
} else if (usagePathStatus === RequestStatus.FAILED) {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { camelCaseObject, ensureConfig, getConfig } from '@edx/frontend-platform';
|
import { camelCaseObject, ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
import saveAs from 'file-saver';
|
||||||
|
|
||||||
ensureConfig([
|
ensureConfig([
|
||||||
'STUDIO_BASE_URL',
|
'STUDIO_BASE_URL',
|
||||||
], 'Course Apps API service');
|
], 'Course Apps API service');
|
||||||
@@ -20,6 +23,62 @@ export async function getAssets(courseId, totalCount) {
|
|||||||
.get(`${getAssetsUrl(courseId)}?page_size=${pageCount}`);
|
.get(`${getAssetsUrl(courseId)}?page_size=${pageCount}`);
|
||||||
return camelCaseObject(data);
|
return camelCaseObject(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch asset file.
|
||||||
|
* @param {blockId} courseId Course ID for the course to operate on
|
||||||
|
|
||||||
|
*/
|
||||||
|
export async function getDownload(selectedRows, courseId) {
|
||||||
|
const downloadErrors = [];
|
||||||
|
if (selectedRows?.length > 1) {
|
||||||
|
const zip = new JSZip();
|
||||||
|
const date = new Date().toString();
|
||||||
|
const folder = zip.folder(`${courseId}-assets-${date}`);
|
||||||
|
const assetNames = [];
|
||||||
|
const assetFetcher = await Promise.allSettled(
|
||||||
|
selectedRows.map(async (row) => {
|
||||||
|
const asset = row?.original;
|
||||||
|
try {
|
||||||
|
assetNames.push(asset.displayName);
|
||||||
|
const res = await fetch(`${getApiBaseUrl()}/${asset.id}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
return res.blob();
|
||||||
|
} catch (error) {
|
||||||
|
downloadErrors.push(`Failed to download ${asset?.displayName}.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const definedAssets = assetFetcher.filter(asset => asset.value !== null);
|
||||||
|
if (definedAssets.length > 0) {
|
||||||
|
definedAssets.forEach((assetBlob, index) => {
|
||||||
|
folder.file(assetNames[index], assetBlob.value, { blob: true });
|
||||||
|
});
|
||||||
|
zip.generateAsync({ type: 'blob' }).then(content => {
|
||||||
|
saveAs(content, `${courseId}-assets-${date}.zip`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (selectedRows?.length === 1) {
|
||||||
|
const asset = selectedRows[0].original;
|
||||||
|
try {
|
||||||
|
saveAs(`${getApiBaseUrl()}/${asset.id}`, asset.displayName);
|
||||||
|
} catch (error) {
|
||||||
|
downloadErrors.push(`Failed to download ${asset?.displayName}.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadErrors.push('No files were selected to download');
|
||||||
|
}
|
||||||
|
return downloadErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch where asset is used in a course.
|
||||||
|
* @param {blockId} courseId Course ID for the course to operate on
|
||||||
|
|
||||||
|
*/
|
||||||
export async function getAssetUsagePaths({ courseId, assetId }) {
|
export async function getAssetUsagePaths({ courseId, assetId }) {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.get(`${getAssetsUrl(courseId)}${assetId}/usage`);
|
.get(`${getAssetsUrl(courseId)}${assetId}/usage`);
|
||||||
@@ -27,7 +86,7 @@ export async function getAssetUsagePaths({ courseId, assetId }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete custom page for provided block.
|
* Delete asset to course.
|
||||||
* @param {blockId} courseId Course ID for the course to operate on
|
* @param {blockId} courseId Course ID for the course to operate on
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@@ -37,7 +96,7 @@ export async function deleteAsset(courseId, assetId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add custom page for provided block.
|
* Add asset to course.
|
||||||
* @param {blockId} courseId Course ID for the course to operate on
|
* @param {blockId} courseId Course ID for the course to operate on
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
57
src/files-and-uploads/data/api.test.js
Normal file
57
src/files-and-uploads/data/api.test.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { getDownload } from './api';
|
||||||
|
import 'file-saver';
|
||||||
|
|
||||||
|
jest.mock('file-saver');
|
||||||
|
|
||||||
|
describe('api.js', () => {
|
||||||
|
describe('getDownload', () => {
|
||||||
|
describe('selectedRows length is undefined or less than zero', () => {
|
||||||
|
it('should return with no files selected error if selectedRows is empty', async () => {
|
||||||
|
const expected = ['No files were selected to download'];
|
||||||
|
const actual = await getDownload([], 'courseId');
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
it('should return with no files selected error if selectedRows is null', async () => {
|
||||||
|
const expected = ['No files were selected to download'];
|
||||||
|
const actual = await getDownload(null, 'courseId');
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('selectedRows length is greater than one', () => {
|
||||||
|
it('should not throw error when blob returns null', async () => {
|
||||||
|
const mockResponseData = { ok: true, blob: () => null };
|
||||||
|
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||||
|
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||||
|
const expected = [];
|
||||||
|
const actual = await getDownload([
|
||||||
|
{ original: { displayName: 'test1' } },
|
||||||
|
{ original: { displayName: 'test2', id: '2' } },
|
||||||
|
]);
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
it('should return error if row does not contain .original ancestor', async () => {
|
||||||
|
const mockResponseData = { ok: true, blob: () => 'data' };
|
||||||
|
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||||
|
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||||
|
const expected = ['Failed to download undefined.'];
|
||||||
|
const actual = await getDownload([
|
||||||
|
{ asset: { displayName: 'test1', id: '1' } },
|
||||||
|
{ original: { displayName: 'test2', id: '2' } },
|
||||||
|
]);
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('selectedRows length equals one', () => {
|
||||||
|
it('should return error if row does not contain .original ancestor', async () => {
|
||||||
|
const mockResponseData = { ok: true, blob: () => 'data' };
|
||||||
|
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||||
|
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||||
|
const expected = ['Failed to download undefined.'];
|
||||||
|
const actual = await getDownload([
|
||||||
|
{ asset: { displayName: 'test1', id: '1' } },
|
||||||
|
]);
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,9 +13,10 @@ const slice = createSlice({
|
|||||||
deletingStatus: '',
|
deletingStatus: '',
|
||||||
usageStatus: '',
|
usageStatus: '',
|
||||||
errors: {
|
errors: {
|
||||||
upload: [],
|
add: [],
|
||||||
delete: [],
|
delete: [],
|
||||||
lock: [],
|
lock: [],
|
||||||
|
download: [],
|
||||||
usageMetrics: [],
|
usageMetrics: [],
|
||||||
},
|
},
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
@@ -30,14 +31,27 @@ const slice = createSlice({
|
|||||||
updateLoadingStatus: (state, { payload }) => {
|
updateLoadingStatus: (state, { payload }) => {
|
||||||
state.loadingStatus = payload.status;
|
state.loadingStatus = payload.status;
|
||||||
},
|
},
|
||||||
updateUpdatingStatus: (state, { payload }) => {
|
updateEditStatus: (state, { payload }) => {
|
||||||
state.updatingStatus = payload.status;
|
const { editType, status } = payload;
|
||||||
},
|
switch (editType) {
|
||||||
updateAddingStatus: (state, { payload }) => {
|
case 'delete':
|
||||||
state.addingStatus = payload.status;
|
state.deletingStatus = status;
|
||||||
},
|
break;
|
||||||
updateDeletingStatus: (state, { payload }) => {
|
case 'add':
|
||||||
state.deletingStatus = payload.status;
|
state.addingStatus = status;
|
||||||
|
break;
|
||||||
|
case 'lock':
|
||||||
|
state.updatingStatus = status;
|
||||||
|
break;
|
||||||
|
case 'download':
|
||||||
|
state.updatingStatus = status;
|
||||||
|
break;
|
||||||
|
case 'usageMetrics':
|
||||||
|
state.usageStatus = status;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteAssetSuccess: (state, { payload }) => {
|
deleteAssetSuccess: (state, { payload }) => {
|
||||||
state.assetIds = state.assetIds.filter(id => id !== payload.assetId);
|
state.assetIds = state.assetIds.filter(id => id !== payload.assetId);
|
||||||
@@ -45,14 +59,15 @@ const slice = createSlice({
|
|||||||
addAssetSuccess: (state, { payload }) => {
|
addAssetSuccess: (state, { payload }) => {
|
||||||
state.assetIds = [payload.assetId, ...state.assetIds];
|
state.assetIds = [payload.assetId, ...state.assetIds];
|
||||||
},
|
},
|
||||||
updateUsageStatus: (state, { payload }) => {
|
|
||||||
state.usageStatus = payload.status;
|
|
||||||
},
|
|
||||||
updateErrors: (state, { payload }) => {
|
updateErrors: (state, { payload }) => {
|
||||||
const { error, message } = payload;
|
const { error, message } = payload;
|
||||||
const currentErrorState = state.errors[error];
|
const currentErrorState = state.errors[error];
|
||||||
state.errors[error] = [...currentErrorState, message];
|
state.errors[error] = [...currentErrorState, message];
|
||||||
},
|
},
|
||||||
|
clearErrors: (state, { payload }) => {
|
||||||
|
const { error } = payload;
|
||||||
|
state.errors[error] = [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,13 +75,11 @@ export const {
|
|||||||
setAssetIds,
|
setAssetIds,
|
||||||
setTotalCount,
|
setTotalCount,
|
||||||
updateLoadingStatus,
|
updateLoadingStatus,
|
||||||
updateUpdatingStatus,
|
|
||||||
deleteAssetSuccess,
|
deleteAssetSuccess,
|
||||||
updateDeletingStatus,
|
|
||||||
addAssetSuccess,
|
addAssetSuccess,
|
||||||
updateAddingStatus,
|
|
||||||
updateUsageStatus,
|
|
||||||
updateErrors,
|
updateErrors,
|
||||||
|
clearErrors,
|
||||||
|
updateEditStatus,
|
||||||
} = slice.actions;
|
} = slice.actions;
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isEmpty } from 'lodash';
|
||||||
import { RequestStatus } from '../../data/constants';
|
import { RequestStatus } from '../../data/constants';
|
||||||
import {
|
import {
|
||||||
addModel,
|
addModel,
|
||||||
@@ -11,18 +12,17 @@ import {
|
|||||||
addAsset,
|
addAsset,
|
||||||
deleteAsset,
|
deleteAsset,
|
||||||
updateLockStatus,
|
updateLockStatus,
|
||||||
|
getDownload,
|
||||||
} from './api';
|
} from './api';
|
||||||
import {
|
import {
|
||||||
setAssetIds,
|
setAssetIds,
|
||||||
setTotalCount,
|
setTotalCount,
|
||||||
updateLoadingStatus,
|
updateLoadingStatus,
|
||||||
updateUpdatingStatus,
|
|
||||||
deleteAssetSuccess,
|
deleteAssetSuccess,
|
||||||
updateDeletingStatus,
|
|
||||||
addAssetSuccess,
|
addAssetSuccess,
|
||||||
updateAddingStatus,
|
|
||||||
updateErrors,
|
updateErrors,
|
||||||
updateUsageStatus,
|
clearErrors,
|
||||||
|
updateEditStatus,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
|
|
||||||
import { updateFileValues } from './utils';
|
import { updateFileValues } from './utils';
|
||||||
@@ -61,24 +61,24 @@ export function updateAssetOrder(courseId, assetIds) {
|
|||||||
|
|
||||||
export function deleteAssetFile(courseId, id, totalCount) {
|
export function deleteAssetFile(courseId, id, totalCount) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(updateDeletingStatus({ status: RequestStatus.IN_PROGRESS }));
|
dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.IN_PROGRESS }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteAsset(courseId, id);
|
await deleteAsset(courseId, id);
|
||||||
dispatch(deleteAssetSuccess({ assetId: id }));
|
dispatch(deleteAssetSuccess({ assetId: id }));
|
||||||
dispatch(removeModel({ modelType: 'assets', id }));
|
dispatch(removeModel({ modelType: 'assets', id }));
|
||||||
dispatch(setTotalCount({ totalCount: totalCount - 1 }));
|
dispatch(setTotalCount({ totalCount: totalCount - 1 }));
|
||||||
dispatch(updateDeletingStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.SUCCESSFUL }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(updateErrors({ error: 'delete', message: `Failed to delete file id ${id}.` }));
|
dispatch(updateErrors({ error: 'delete', message: `Failed to delete file id ${id}.` }));
|
||||||
dispatch(updateDeletingStatus({ status: RequestStatus.FAILED }));
|
dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.FAILED }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addAssetFile(courseId, file, totalCount) {
|
export function addAssetFile(courseId, file, totalCount) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(updateAddingStatus({ status: RequestStatus.IN_PROGRESS }));
|
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.IN_PROGRESS }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { asset } = await addAsset(courseId, file);
|
const { asset } = await addAsset(courseId, file);
|
||||||
@@ -91,22 +91,22 @@ export function addAssetFile(courseId, file, totalCount) {
|
|||||||
assetId: asset.id,
|
assetId: asset.id,
|
||||||
}));
|
}));
|
||||||
dispatch(setTotalCount({ totalCount: totalCount + 1 }));
|
dispatch(setTotalCount({ totalCount: totalCount + 1 }));
|
||||||
dispatch(updateAddingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.SUCCESSFUL }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response && error.response.status === 413) {
|
if (error.response && error.response.status === 413) {
|
||||||
const message = error.response.data.error;
|
const message = error.response.data.error;
|
||||||
dispatch(updateErrors({ error: 'upload', message }));
|
dispatch(updateErrors({ error: 'add', message }));
|
||||||
} else {
|
} else {
|
||||||
dispatch(updateErrors({ error: 'upload', message: `Failed to add ${file.name}.` }));
|
dispatch(updateErrors({ error: 'add', message: `Failed to add ${file.name}.` }));
|
||||||
}
|
}
|
||||||
dispatch(updateAddingStatus({ status: RequestStatus.FAILED }));
|
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAssetLock({ assetId, courseId, locked }) {
|
export function updateAssetLock({ assetId, courseId, locked }) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(updateUpdatingStatus({ status: RequestStatus.IN_PROGRESS }));
|
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.IN_PROGRESS }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateLockStatus({ assetId, courseId, locked });
|
await updateLockStatus({ assetId, courseId, locked });
|
||||||
@@ -117,26 +117,45 @@ export function updateAssetLock({ assetId, courseId, locked }) {
|
|||||||
locked,
|
locked,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
dispatch(updateUpdatingStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.SUCCESSFUL }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const lockStatus = locked ? 'lock' : 'unlock';
|
const lockStatus = locked ? 'lock' : 'unlock';
|
||||||
dispatch(updateErrors({ error: 'lock', message: `Failed to ${lockStatus} file id ${assetId}.` }));
|
dispatch(updateErrors({ error: 'lock', message: `Failed to ${lockStatus} file id ${assetId}.` }));
|
||||||
dispatch(updateUpdatingStatus({ status: RequestStatus.FAILED }));
|
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.FAILED }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetErrors({ errorType }) {
|
||||||
|
return (dispatch) => { dispatch(clearErrors({ error: errorType })); };
|
||||||
|
}
|
||||||
|
|
||||||
export function getUsagePaths({ asset, courseId, setSelectedRows }) {
|
export function getUsagePaths({ asset, courseId, setSelectedRows }) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(updateUsageStatus({ status: RequestStatus.IN_PROGRESS }));
|
dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.IN_PROGRESS }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { usageLocations } = await getAssetUsagePaths({ assetId: asset.id, courseId });
|
const { usageLocations } = await getAssetUsagePaths({ assetId: asset.id, courseId });
|
||||||
setSelectedRows([{ original: { ...asset, usageLocations } }]);
|
setSelectedRows([{ original: { ...asset, usageLocations } }]);
|
||||||
dispatch(updateUsageStatus({ status: RequestStatus.SUCCESSFUL }));
|
dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.SUCCESSFUL }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(updateErrors({ error: 'usageMetrics', message: `Failed to get usage metrics for ${asset.displayName}.` }));
|
dispatch(updateErrors({ error: 'usageMetrics', message: `Failed to get usage metrics for ${asset.displayName}.` }));
|
||||||
dispatch(updateUsageStatus({ status: RequestStatus.FAILED }));
|
dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.FAILED }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAssetDownload({ selectedRows, courseId }) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.IN_PROGRESS }));
|
||||||
|
const errors = await getDownload(selectedRows, courseId);
|
||||||
|
if (isEmpty(errors)) {
|
||||||
|
dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.SUCCESSFUL }));
|
||||||
|
} else {
|
||||||
|
errors.forEach(error => {
|
||||||
|
dispatch(updateErrors({ error: 'download', message: error }));
|
||||||
|
});
|
||||||
|
dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.FAILED }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ export const initialState = {
|
|||||||
addingStatus: '',
|
addingStatus: '',
|
||||||
usageStatus: '',
|
usageStatus: '',
|
||||||
errors: {
|
errors: {
|
||||||
upload: [],
|
add: [],
|
||||||
delete: [],
|
delete: [],
|
||||||
lock: [],
|
lock: [],
|
||||||
|
download: [],
|
||||||
usageMetrics: [],
|
usageMetrics: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getSrc } from '../data/utils';
|
|||||||
const GalleryCard = ({
|
const GalleryCard = ({
|
||||||
className,
|
className,
|
||||||
original,
|
original,
|
||||||
|
handleBulkDownload,
|
||||||
handleLockedAsset,
|
handleLockedAsset,
|
||||||
handleOpenDeleteConfirmation,
|
handleOpenDeleteConfirmation,
|
||||||
handleOpenAssetInfo,
|
handleOpenAssetInfo,
|
||||||
@@ -43,6 +44,9 @@ const GalleryCard = ({
|
|||||||
portableUrl={original.portableUrl}
|
portableUrl={original.portableUrl}
|
||||||
iconSrc={MoreVert}
|
iconSrc={MoreVert}
|
||||||
id={original.id}
|
id={original.id}
|
||||||
|
onDownload={() => handleBulkDownload(
|
||||||
|
[{ original: { id: original.id, displayName: original.displayName } }],
|
||||||
|
)}
|
||||||
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
||||||
/>
|
/>
|
||||||
</ActionRow>
|
</ActionRow>
|
||||||
@@ -87,6 +91,7 @@ GalleryCard.propTypes = {
|
|||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
portableUrl: PropTypes.string.isRequired,
|
portableUrl: PropTypes.string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
handleBulkDownload: PropTypes.func.isRequired,
|
||||||
handleLockedAsset: PropTypes.func.isRequired,
|
handleLockedAsset: PropTypes.func.isRequired,
|
||||||
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
|
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
|
||||||
handleOpenAssetInfo: PropTypes.func.isRequired,
|
handleOpenAssetInfo: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getSrc } from '../data/utils';
|
|||||||
const ListCard = ({
|
const ListCard = ({
|
||||||
className,
|
className,
|
||||||
original,
|
original,
|
||||||
|
handleBulkDownload,
|
||||||
handleLockedAsset,
|
handleLockedAsset,
|
||||||
handleOpenDeleteConfirmation,
|
handleOpenDeleteConfirmation,
|
||||||
handleOpenAssetInfo,
|
handleOpenAssetInfo,
|
||||||
@@ -67,6 +68,9 @@ const ListCard = ({
|
|||||||
portableUrl={original.portableUrl}
|
portableUrl={original.portableUrl}
|
||||||
iconSrc={MoreVert}
|
iconSrc={MoreVert}
|
||||||
id={original.id}
|
id={original.id}
|
||||||
|
onDownload={() => handleBulkDownload(
|
||||||
|
[{ original: { id: original.id, displayName: original.displayName } }],
|
||||||
|
)}
|
||||||
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
||||||
/>
|
/>
|
||||||
</ActionRow>
|
</ActionRow>
|
||||||
@@ -89,6 +93,7 @@ ListCard.propTypes = {
|
|||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
portableUrl: PropTypes.string.isRequired,
|
portableUrl: PropTypes.string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
handleBulkDownload: PropTypes.func.isRequired,
|
||||||
handleLockedAsset: PropTypes.func.isRequired,
|
handleLockedAsset: PropTypes.func.isRequired,
|
||||||
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
|
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
|
||||||
handleOpenAssetInfo: PropTypes.func.isRequired,
|
handleOpenAssetInfo: PropTypes.func.isRequired,
|
||||||
|
|||||||
107
src/generic/course-stepper/CourseStepper.test.jsx
Normal file
107
src/generic/course-stepper/CourseStepper.test.jsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import CourseStepper from '.';
|
||||||
|
|
||||||
|
const stepsMock = [
|
||||||
|
{
|
||||||
|
title: 'Preparing',
|
||||||
|
description: 'Preparing to start the export',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Exporting',
|
||||||
|
description: 'Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Compressing',
|
||||||
|
description: 'Compressing the exported data and preparing it for download',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Success',
|
||||||
|
description: 'Your exported course can now be downloaded',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderComponent = (props) => render(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<CourseStepper steps={stepsMock} {...props} />
|
||||||
|
</IntlProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<CourseStepper />', () => {
|
||||||
|
it('renders CourseStepper correctly', () => {
|
||||||
|
const {
|
||||||
|
getByText, getByTestId, getAllByTestId, queryByTestId,
|
||||||
|
} = renderComponent({ activeKey: 0 });
|
||||||
|
|
||||||
|
const steps = getAllByTestId('course-stepper__step');
|
||||||
|
expect(steps.length).toBe(stepsMock.length);
|
||||||
|
|
||||||
|
stepsMock.forEach((step) => {
|
||||||
|
expect(getByText(step.title)).toBeInTheDocument();
|
||||||
|
expect(getByText(step.description)).toBeInTheDocument();
|
||||||
|
expect(getByTestId(`${step.title}-icon`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const percentElement = queryByTestId('course-stepper__step-percent');
|
||||||
|
expect(percentElement).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks the active and done steps correctly', () => {
|
||||||
|
const activeKey = 1;
|
||||||
|
const { getAllByTestId } = renderComponent({ activeKey });
|
||||||
|
|
||||||
|
const steps = getAllByTestId('course-stepper__step');
|
||||||
|
stepsMock.forEach((_, index) => {
|
||||||
|
const stepElement = steps[index];
|
||||||
|
if (index === activeKey) {
|
||||||
|
expect(stepElement).toHaveClass('active');
|
||||||
|
expect(stepElement).not.toHaveClass('done');
|
||||||
|
}
|
||||||
|
if (index < activeKey) {
|
||||||
|
expect(stepElement).not.toHaveClass('active');
|
||||||
|
expect(stepElement).toHaveClass('done');
|
||||||
|
}
|
||||||
|
if (index > activeKey) {
|
||||||
|
expect(stepElement).not.toHaveClass('active');
|
||||||
|
expect(stepElement).not.toHaveClass('done');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mark the error step correctly', () => {
|
||||||
|
const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true });
|
||||||
|
|
||||||
|
const errorStep = getAllByTestId('course-stepper__step')[1];
|
||||||
|
expect(errorStep).toHaveClass('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message for error step', () => {
|
||||||
|
const errorMessage = 'Some error text';
|
||||||
|
const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true, errorMessage });
|
||||||
|
|
||||||
|
const errorStep = getAllByTestId('course-stepper__step')[1];
|
||||||
|
expect(errorStep).toHaveClass('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows percentage for active step', () => {
|
||||||
|
const percent = 50;
|
||||||
|
const { getByTestId } = renderComponent({ activeKey: 1, percent });
|
||||||
|
|
||||||
|
const percentElement = getByTestId('course-stepper__step-percent');
|
||||||
|
expect(percentElement).toBeInTheDocument();
|
||||||
|
expect(percentElement).toHaveTextContent(`${percent}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows null when steps length equal to zero', () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<CourseStepper steps={[]} activeKey={0} />
|
||||||
|
</IntlProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const steps = queryByTestId('[data-testid="course-stepper__step"]');
|
||||||
|
expect(steps).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
68
src/generic/course-stepper/CouseStepper.scss
Normal file
68
src/generic/course-stepper/CouseStepper.scss
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.course-stepper {
|
||||||
|
.course-stepper__step {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacer;
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
opacity: .5;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step-icon {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
position: absolute;
|
||||||
|
top: .875rem;
|
||||||
|
left: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step-info {
|
||||||
|
margin-left: 1.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step-title {
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step-percent {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step-description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: $gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step.active {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
animation: rotate 2s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step.done {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
& svg,
|
||||||
|
.course-stepper__step-title {
|
||||||
|
color: $success-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-stepper__step.error {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.course-stepper__step-title,
|
||||||
|
.course-stepper__step-description,
|
||||||
|
& svg {
|
||||||
|
color: $danger-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/generic/course-stepper/index.jsx
Normal file
118
src/generic/course-stepper/index.jsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import {
|
||||||
|
Settings as SettingsIcon,
|
||||||
|
ManageHistory as SuccessIcon,
|
||||||
|
Warning as ErrorIcon,
|
||||||
|
CheckCircle,
|
||||||
|
} from '@edx/paragon/icons';
|
||||||
|
import { Icon } from '@edx/paragon';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const CourseStepper = ({
|
||||||
|
steps,
|
||||||
|
activeKey,
|
||||||
|
percent,
|
||||||
|
hasError,
|
||||||
|
errorMessage,
|
||||||
|
}) => {
|
||||||
|
const getStepperSettings = (index) => {
|
||||||
|
const lastStepIndex = steps.length - 1;
|
||||||
|
const isActiveStep = index === activeKey;
|
||||||
|
const isLastStep = index === lastStepIndex;
|
||||||
|
const isErrorStep = isActiveStep && hasError;
|
||||||
|
const isLastStepDone = isLastStep && isActiveStep;
|
||||||
|
const completedStep = index < activeKey && !hasError;
|
||||||
|
|
||||||
|
const getStepIcon = () => {
|
||||||
|
if (completedStep) {
|
||||||
|
return CheckCircle;
|
||||||
|
}
|
||||||
|
if (hasError && isActiveStep) {
|
||||||
|
return ErrorIcon;
|
||||||
|
}
|
||||||
|
if (isLastStep && !isActiveStep) {
|
||||||
|
return SuccessIcon;
|
||||||
|
}
|
||||||
|
if (isLastStepDone) {
|
||||||
|
return CheckCircle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepIcon: getStepIcon(index),
|
||||||
|
isPercentShow: Boolean(percent) && percent !== 100 && isActiveStep && !hasError,
|
||||||
|
isErrorMessageShow: isErrorStep && errorMessage,
|
||||||
|
isActiveClass: isActiveStep && !isLastStep && !hasError,
|
||||||
|
isDoneClass: index < activeKey || isLastStepDone,
|
||||||
|
isErrorClass: isErrorStep,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="course-stepper">
|
||||||
|
{steps.length ? steps.map(({ title, description }, index) => {
|
||||||
|
const {
|
||||||
|
stepIcon,
|
||||||
|
isPercentShow,
|
||||||
|
isErrorMessageShow,
|
||||||
|
isActiveClass,
|
||||||
|
isDoneClass,
|
||||||
|
isErrorClass,
|
||||||
|
} = getStepperSettings(index);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('course-stepper__step', {
|
||||||
|
active: isActiveClass,
|
||||||
|
done: isDoneClass,
|
||||||
|
error: isErrorClass,
|
||||||
|
})}
|
||||||
|
key={title}
|
||||||
|
data-testid="course-stepper__step"
|
||||||
|
>
|
||||||
|
<div className="course-stepper__step-icon">
|
||||||
|
<Icon src={stepIcon} alt={title} data-testid={`${title}-icon`} />
|
||||||
|
</div>
|
||||||
|
<div className="course-stepper__step-info">
|
||||||
|
<h3 className="h4 title course-stepper__step-title font-weight-600">{title}</h3>
|
||||||
|
{isPercentShow && (
|
||||||
|
<p
|
||||||
|
className="course-stepper__step-percent font-weight-400"
|
||||||
|
data-testid="course-stepper__step-percent"
|
||||||
|
>
|
||||||
|
{percent}%
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="course-stepper__step-description font-weight-400">
|
||||||
|
{isErrorMessageShow ? errorMessage : description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CourseStepper.defaultProps = {
|
||||||
|
percent: false,
|
||||||
|
hasError: false,
|
||||||
|
errorMessage: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
CourseStepper.propTypes = {
|
||||||
|
steps: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string.isRequired,
|
||||||
|
})).isRequired,
|
||||||
|
activeKey: PropTypes.number.isRequired,
|
||||||
|
percent: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CourseStepper);
|
||||||
296
src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx
Normal file
296
src/generic/create-or-rerun-course/CreateOrRerunCourseForm.jsx
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
ActionRow,
|
||||||
|
StatefulButton,
|
||||||
|
TransitionReplace,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { Info as InfoIcon } from '@edx/paragon/icons';
|
||||||
|
import { TypeaheadDropdown } from '@edx/frontend-lib-content-components';
|
||||||
|
|
||||||
|
import AlertMessage from '../alert-message';
|
||||||
|
import { STATEFUL_BUTTON_STATES } from '../../constants';
|
||||||
|
import { RequestStatus } from '../../data/constants';
|
||||||
|
import { getSavingStatus } from '../data/selectors';
|
||||||
|
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
||||||
|
import { updatePostErrors } from '../data/slice';
|
||||||
|
import { updateCreateOrRerunCourseQuery } from '../data/thunks';
|
||||||
|
import { useCreateOrRerunCourse } from './hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const CreateOrRerunCourseForm = ({
|
||||||
|
title,
|
||||||
|
isCreateNewCourse,
|
||||||
|
initialValues,
|
||||||
|
onClickCancel,
|
||||||
|
}) => {
|
||||||
|
const { courseId } = useParams();
|
||||||
|
const savingStatus = useSelector(getSavingStatus);
|
||||||
|
const { allowToCreateNewOrg } = useSelector(getStudioHomeData);
|
||||||
|
const runFieldReference = useRef(null);
|
||||||
|
const displayNameFieldReference = useRef(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
intl,
|
||||||
|
errors,
|
||||||
|
values,
|
||||||
|
postErrors,
|
||||||
|
isFormFilled,
|
||||||
|
isFormInvalid,
|
||||||
|
organizations,
|
||||||
|
showErrorBanner,
|
||||||
|
dispatch,
|
||||||
|
handleBlur,
|
||||||
|
handleChange,
|
||||||
|
hasErrorField,
|
||||||
|
setFieldValue,
|
||||||
|
} = useCreateOrRerunCourse(initialValues);
|
||||||
|
|
||||||
|
const newCourseFields = [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.courseDisplayNameLabel),
|
||||||
|
helpText: intl.formatMessage(
|
||||||
|
isCreateNewCourse
|
||||||
|
? messages.courseDisplayNameCreateHelpText
|
||||||
|
: messages.courseDisplayNameRerunHelpText,
|
||||||
|
),
|
||||||
|
name: 'displayName',
|
||||||
|
value: values.displayName,
|
||||||
|
placeholder: intl.formatMessage(messages.courseDisplayNamePlaceholder),
|
||||||
|
disabled: false,
|
||||||
|
ref: displayNameFieldReference,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.courseOrgLabel),
|
||||||
|
helpText: isCreateNewCourse
|
||||||
|
? intl.formatMessage(messages.courseOrgCreateHelpText, {
|
||||||
|
strong: <strong>{intl.formatMessage(messages.courseNoteOrgNameIsPartStrong)}</strong>,
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.courseOrgRerunHelpText, {
|
||||||
|
strong: (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<strong>
|
||||||
|
{intl.formatMessage(messages.courseNoteNoSpaceAllowedStrong)}
|
||||||
|
</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
name: 'org',
|
||||||
|
value: values.org,
|
||||||
|
options: organizations,
|
||||||
|
placeholder: intl.formatMessage(messages.courseOrgPlaceholder),
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.courseNumberLabel),
|
||||||
|
helpText: isCreateNewCourse
|
||||||
|
? intl.formatMessage(messages.courseNumberCreateHelpText, {
|
||||||
|
strong: (
|
||||||
|
<strong>
|
||||||
|
{intl.formatMessage(messages.courseNotePartCourseURLRequireStrong)}
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.courseNumberRerunHelpText),
|
||||||
|
name: 'number',
|
||||||
|
value: values.number,
|
||||||
|
placeholder: intl.formatMessage(messages.courseNumberPlaceholder),
|
||||||
|
disabled: !isCreateNewCourse,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.courseRunLabel),
|
||||||
|
helpText: isCreateNewCourse
|
||||||
|
? intl.formatMessage(messages.courseRunCreateHelpText, {
|
||||||
|
strong: (
|
||||||
|
<strong>
|
||||||
|
{intl.formatMessage(messages.courseNotePartCourseURLRequireStrong)}
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.courseRunRerunHelpText, {
|
||||||
|
strong: (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<strong>
|
||||||
|
{intl.formatMessage(messages.courseNoteNoSpaceAllowedStrong)}
|
||||||
|
</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
name: 'run',
|
||||||
|
value: values.run,
|
||||||
|
placeholder: intl.formatMessage(messages.courseRunPlaceholder),
|
||||||
|
disabled: false,
|
||||||
|
ref: runFieldReference,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const createButtonState = {
|
||||||
|
labels: {
|
||||||
|
default: intl.formatMessage(isCreateNewCourse ? messages.createButton : messages.rerunCreateButton),
|
||||||
|
pending: intl.formatMessage(isCreateNewCourse ? messages.creatingButton : messages.rerunningCreateButton),
|
||||||
|
},
|
||||||
|
disabledStates: [STATEFUL_BUTTON_STATES.pending],
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnClickCreate = () => {
|
||||||
|
const courseData = isCreateNewCourse ? values : { ...values, sourceCourseKey: courseId };
|
||||||
|
dispatch(updateCreateOrRerunCourseQuery(courseData));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnClickCancel = () => {
|
||||||
|
dispatch(updatePostErrors({}));
|
||||||
|
onClickCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomBlurForDropdown = (e) => {
|
||||||
|
// it needs to correct handleOnChange Form.Autosuggest
|
||||||
|
const { value, name } = e.target;
|
||||||
|
setFieldValue(name, value);
|
||||||
|
handleBlur(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderOrgField = (field) => (allowToCreateNewOrg ? (
|
||||||
|
<TypeaheadDropdown
|
||||||
|
readOnly={false}
|
||||||
|
name={field.name}
|
||||||
|
value={field.value}
|
||||||
|
controlClassName={classNames({ 'is-invalid': hasErrorField(field.name) })}
|
||||||
|
options={field.options}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
handleBlur={handleCustomBlurForDropdown}
|
||||||
|
handleChange={(value) => setFieldValue(field.name, value)}
|
||||||
|
noOptionsMessage={intl.formatMessage(messages.courseOrgNoOptions)}
|
||||||
|
helpMessage=""
|
||||||
|
errorMessage=""
|
||||||
|
floatingLabel=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Dropdown className="mr-2">
|
||||||
|
<Dropdown.Toggle id={`${field.name}-dropdown`} variant="outline-primary">
|
||||||
|
{field.value || intl.formatMessage(messages.courseOrgNoOptions)}
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{field.options?.map((value) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={value}
|
||||||
|
onClick={() => setFieldValue(field.name, value)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// it needs to display the initial focus for the field depending on the current page
|
||||||
|
if (!isCreateNewCourse) {
|
||||||
|
runFieldReference?.current?.focus();
|
||||||
|
} else {
|
||||||
|
displayNameFieldReference?.current?.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="create-or-rerun-course-form">
|
||||||
|
<TransitionReplace>
|
||||||
|
{showErrorBanner ? (
|
||||||
|
<AlertMessage
|
||||||
|
variant="danger"
|
||||||
|
icon={InfoIcon}
|
||||||
|
title={postErrors.errMsg}
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-labelledby={intl.formatMessage(
|
||||||
|
messages.alertErrorExistsAriaLabelledBy,
|
||||||
|
)}
|
||||||
|
aria-describedby={intl.formatMessage(
|
||||||
|
messages.alertErrorExistsAriaDescribedBy,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TransitionReplace>
|
||||||
|
<h3 className="mb-3">{title}</h3>
|
||||||
|
<Form>
|
||||||
|
{newCourseFields.map((field) => (
|
||||||
|
<Form.Group
|
||||||
|
className={classNames('form-group-custom', {
|
||||||
|
'form-group-custom_isInvalid': hasErrorField(field.name),
|
||||||
|
})}
|
||||||
|
key={field.label}
|
||||||
|
>
|
||||||
|
<Form.Label>{field.label}</Form.Label>
|
||||||
|
{field.name !== 'org' ? (
|
||||||
|
<Form.Control
|
||||||
|
value={field.value}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
name={field.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
isInvalid={hasErrorField(field.name)}
|
||||||
|
disabled={field.disabled}
|
||||||
|
ref={field?.ref}
|
||||||
|
/>
|
||||||
|
) : renderOrgField(field)}
|
||||||
|
<Form.Text>{field.helpText}</Form.Text>
|
||||||
|
{hasErrorField(field.name) && (
|
||||||
|
<Form.Control.Feedback
|
||||||
|
className="feedback-error"
|
||||||
|
type="invalid"
|
||||||
|
hasIcon={false}
|
||||||
|
>
|
||||||
|
{errors[field.name]}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
|
))}
|
||||||
|
<ActionRow className="justify-content-start">
|
||||||
|
<Button
|
||||||
|
variant="outline-primary"
|
||||||
|
onClick={handleOnClickCancel}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.cancelButton)}
|
||||||
|
</Button>
|
||||||
|
<StatefulButton
|
||||||
|
key="save-button"
|
||||||
|
className="ml-3"
|
||||||
|
onClick={handleOnClickCreate}
|
||||||
|
disabled={!isFormFilled || isFormInvalid}
|
||||||
|
state={
|
||||||
|
savingStatus === RequestStatus.PENDING
|
||||||
|
? STATEFUL_BUTTON_STATES.pending
|
||||||
|
: STATEFUL_BUTTON_STATES.default
|
||||||
|
}
|
||||||
|
{...createButtonState}
|
||||||
|
/>
|
||||||
|
</ActionRow>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CreateOrRerunCourseForm.defaultProps = {
|
||||||
|
title: '',
|
||||||
|
isCreateNewCourse: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
CreateOrRerunCourseForm.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
initialValues: PropTypes.shape({
|
||||||
|
displayName: PropTypes.string.isRequired,
|
||||||
|
org: PropTypes.string.isRequired,
|
||||||
|
number: PropTypes.string.isRequired,
|
||||||
|
run: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
isCreateNewCourse: PropTypes.bool,
|
||||||
|
onClickCancel: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateOrRerunCourseForm;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.create-or-rerun-course-form {
|
||||||
|
.form-group-custom {
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pgn__form-label {
|
||||||
|
font: normal 1.125rem/1.75rem $font-family-base;
|
||||||
|
color: $gray-700;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pgn__form-control-description,
|
||||||
|
.pgn__form-text {
|
||||||
|
margin-top: .62rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
screen,
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
|
import { studioHomeMock } from '../../studio-home/__mocks__';
|
||||||
|
import { getStudioHomeApiUrl } from '../../studio-home/data/api';
|
||||||
|
import { fetchStudioHomeData } from '../../studio-home/data/thunks';
|
||||||
|
import { RequestStatus } from '../../data/constants';
|
||||||
|
import initializeStore from '../../store';
|
||||||
|
import { executeThunk } from '../../utils';
|
||||||
|
import { updateCreateOrRerunCourseQuery } from '../data/thunks';
|
||||||
|
import { getCreateOrRerunCourseUrl } from '../data/api';
|
||||||
|
import messages from './messages';
|
||||||
|
import { CreateOrRerunCourseForm } from '.';
|
||||||
|
import { initialState } from './factories/mockApiResponses';
|
||||||
|
|
||||||
|
jest.mock('react-router', () => ({
|
||||||
|
...jest.requireActual('react-router'),
|
||||||
|
useParams: () => ({
|
||||||
|
courseId: 'course-id-mock',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
ReactDOM.createPortal = jest.fn(node => node);
|
||||||
|
|
||||||
|
const onClickCancelMock = jest.fn();
|
||||||
|
|
||||||
|
const RootWrapper = (props) => (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<CreateOrRerunCourseForm {...props} />
|
||||||
|
</AppProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
title: 'Mocked title',
|
||||||
|
isCreateNewCourse: true,
|
||||||
|
initialValues: {
|
||||||
|
displayName: '',
|
||||||
|
org: '',
|
||||||
|
number: '',
|
||||||
|
run: '',
|
||||||
|
},
|
||||||
|
onClickCancel: onClickCancelMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockStore = async () => {
|
||||||
|
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
|
||||||
|
|
||||||
|
await executeThunk(fetchStudioHomeData, store.dispatch);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<CreateOrRerunCourseForm />', () => {
|
||||||
|
afterEach(() => jest.clearAllMocks());
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store = initializeStore(initialState);
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders form successfully', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
|
||||||
|
expect(screen.getByText(props.title)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.courseDisplayNameLabel.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText(messages.courseOrgLabel.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.courseOrgNoOptions.defaultMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText(messages.courseNumberLabel.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText(messages.courseRunLabel.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders create course form with help text successfully', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
expect(screen.getByText(messages.courseDisplayNameCreateHelpText.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('The name of the organization sponsoring the course.', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('The unique number that identifies your course within your organization.', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('The term in which your course will run.', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: messages.createButton.defaultMessage })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders rerun course form with help text successfully', async () => {
|
||||||
|
const initialProps = { ...props, isCreateNewCourse: false };
|
||||||
|
render(<RootWrapper {...initialProps} />);
|
||||||
|
await mockStore();
|
||||||
|
|
||||||
|
expect(screen.getByText(messages.courseDisplayNameRerunHelpText.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('The name of the organization sponsoring the new course. (This name is often the same as the original organization name.)', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.courseNumberRerunHelpText.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('The term in which the new course will run. (This value is often different than the original course run value.)', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: messages.rerunCreateButton.defaultMessage })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call handleOnClickCancel if button cancel clicked', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
const cancelBtn = screen.getByRole('button', { name: messages.cancelButton.defaultMessage });
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(cancelBtn);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onClickCancelMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleOnClickCreate', () => {
|
||||||
|
delete window.location;
|
||||||
|
window.location = { assign: jest.fn() };
|
||||||
|
it('should call window.location.assign with url', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
const url = '/course/courseId';
|
||||||
|
const displayNameInput = screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage);
|
||||||
|
const orgInput = screen.getByText(messages.courseOrgNoOptions.defaultMessage);
|
||||||
|
const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||||
|
const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage);
|
||||||
|
const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
userEvent.type(displayNameInput, 'foo course name');
|
||||||
|
fireEvent.click(orgInput);
|
||||||
|
userEvent.type(numberInput, '777');
|
||||||
|
userEvent.type(runInput, '1');
|
||||||
|
userEvent.click(createBtn);
|
||||||
|
});
|
||||||
|
await axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, { url });
|
||||||
|
await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch);
|
||||||
|
|
||||||
|
expect(window.location.assign).toHaveBeenCalledWith(`${process.env.STUDIO_BASE_URL}${url}`);
|
||||||
|
});
|
||||||
|
it('should call window.location.assign with url and destinationCourseKey', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
const url = '/course/';
|
||||||
|
const destinationCourseKey = 'courseKey';
|
||||||
|
const displayNameInput = screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage);
|
||||||
|
const orgInput = screen.getByText(messages.courseOrgNoOptions.defaultMessage);
|
||||||
|
const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||||
|
const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage);
|
||||||
|
const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage });
|
||||||
|
await axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, { url, destinationCourseKey });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
userEvent.type(displayNameInput, 'foo course name');
|
||||||
|
fireEvent.click(orgInput);
|
||||||
|
userEvent.type(numberInput, '777');
|
||||||
|
userEvent.type(runInput, '1');
|
||||||
|
userEvent.click(createBtn);
|
||||||
|
});
|
||||||
|
await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch);
|
||||||
|
|
||||||
|
expect(window.location.assign).toHaveBeenCalledWith(`${process.env.STUDIO_BASE_URL}${url}${destinationCourseKey}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled create button if form not filled', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage });
|
||||||
|
expect(createBtn).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled rerun button if form not filled', async () => {
|
||||||
|
const initialProps = { ...props, isCreateNewCourse: false };
|
||||||
|
render(<RootWrapper {...initialProps} />);
|
||||||
|
await mockStore();
|
||||||
|
const rerunBtn = screen.getByRole('button', { name: messages.rerunCreateButton.defaultMessage });
|
||||||
|
expect(rerunBtn).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled create button if form has error', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage });
|
||||||
|
const displayNameInput = screen.getByPlaceholderText(messages.courseDisplayNamePlaceholder.defaultMessage);
|
||||||
|
const orgInput = screen.getByText(messages.courseOrgNoOptions.defaultMessage);
|
||||||
|
const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||||
|
const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(displayNameInput, { target: { value: 'foo course name' } });
|
||||||
|
fireEvent.click(orgInput);
|
||||||
|
fireEvent.change(numberInput, { target: { value: 'number with invalid (+) symbol' } });
|
||||||
|
fireEvent.change(runInput, { target: { value: 'number with invalid (=) symbol' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(createBtn).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows typeahead dropdown with allowed to create org permissions', async () => {
|
||||||
|
const updatedStudioData = { ...studioHomeMock, allowToCreateNewOrg: true };
|
||||||
|
store = initializeStore({
|
||||||
|
...initialState,
|
||||||
|
studioHome: {
|
||||||
|
...initialState.studioHome,
|
||||||
|
studioHomeData: updatedStudioData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText(messages.courseOrgPlaceholder.defaultMessage));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows button pending state', async () => {
|
||||||
|
store = initializeStore({
|
||||||
|
...initialState,
|
||||||
|
generic: {
|
||||||
|
...initialState.generic,
|
||||||
|
savingStatus: RequestStatus.PENDING,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
expect(screen.getByRole('button', { name: messages.creatingButton.defaultMessage })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows alert error if postErrors presents', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
await axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, { errMsg: 'aaa' });
|
||||||
|
await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch);
|
||||||
|
|
||||||
|
expect(screen.getByText('aaa')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error on field', async () => {
|
||||||
|
render(<RootWrapper {...props} />);
|
||||||
|
await mockStore();
|
||||||
|
const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(numberInput, { target: { value: 'number with invalid (+) symbol' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(screen.getByText(messages.noSpaceError)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { RequestStatus } from '../../../data/constants';
|
||||||
|
import { studioHomeMock } from '../../../studio-home/__mocks__';
|
||||||
|
|
||||||
|
export const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
|
||||||
|
export const initialState = {
|
||||||
|
generic: {
|
||||||
|
createOrRerunCourse: {
|
||||||
|
courseData: {},
|
||||||
|
courseRerunData: {},
|
||||||
|
redirectUrlObj: {},
|
||||||
|
postErrors: {},
|
||||||
|
},
|
||||||
|
loadingStatuses: {
|
||||||
|
organizationLoadingStatus: 'successful', courseRerunLoadingStatus: 'successful',
|
||||||
|
},
|
||||||
|
organizations: ['krisEdx', 'krisEd', 'DeveloperInc', 'importMit', 'testX', 'edX', 'developerInb'],
|
||||||
|
savingStatus: '',
|
||||||
|
},
|
||||||
|
studioHome: {
|
||||||
|
loadingStatuses: {
|
||||||
|
studioHomeLoadingStatus: RequestStatus.SUCCESSFUL,
|
||||||
|
courseNotificationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||||
|
},
|
||||||
|
savingStatuses: {
|
||||||
|
courseCreatorSavingStatus: '',
|
||||||
|
deleteNotificationSavingStatus: '',
|
||||||
|
},
|
||||||
|
studioHomeData: studioHomeMock,
|
||||||
|
},
|
||||||
|
};
|
||||||
128
src/generic/create-or-rerun-course/hooks.jsx
Normal file
128
src/generic/create-or-rerun-course/hooks.jsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
import { RequestStatus } from '../../data/constants';
|
||||||
|
import { getStudioHomeData } from '../../studio-home/data/selectors';
|
||||||
|
import {
|
||||||
|
getRedirectUrlObj,
|
||||||
|
getOrganizations,
|
||||||
|
getPostErrors,
|
||||||
|
getSavingStatus,
|
||||||
|
} from '../data/selectors';
|
||||||
|
import { updateSavingStatus, updatePostErrors } from '../data/slice';
|
||||||
|
import { fetchOrganizationsQuery } from '../data/thunks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const useCreateOrRerunCourse = (initialValues) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const redirectUrlObj = useSelector(getRedirectUrlObj);
|
||||||
|
const createOrRerunCourseSavingStatus = useSelector(getSavingStatus);
|
||||||
|
const allOrganizations = useSelector(getOrganizations);
|
||||||
|
const postErrors = useSelector(getPostErrors);
|
||||||
|
const {
|
||||||
|
allowToCreateNewOrg,
|
||||||
|
allowedOrganizations,
|
||||||
|
} = useSelector(getStudioHomeData);
|
||||||
|
const [isFormFilled, setFormFilled] = useState(false);
|
||||||
|
const [showErrorBanner, setShowErrorBanner] = useState(false);
|
||||||
|
const organizations = allowToCreateNewOrg ? allOrganizations : allowedOrganizations;
|
||||||
|
const specialCharsRule = /^[a-zA-Z0-9_\-.'*~\s]+$/;
|
||||||
|
const noSpaceRule = /^\S*$/;
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
displayName: Yup.string().required(
|
||||||
|
intl.formatMessage(messages.requiredFieldError),
|
||||||
|
),
|
||||||
|
org: Yup.string()
|
||||||
|
.required(intl.formatMessage(messages.requiredFieldError))
|
||||||
|
.matches(
|
||||||
|
specialCharsRule,
|
||||||
|
intl.formatMessage(messages.disallowedCharsError),
|
||||||
|
)
|
||||||
|
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
||||||
|
number: Yup.string()
|
||||||
|
.required(intl.formatMessage(messages.requiredFieldError))
|
||||||
|
.matches(
|
||||||
|
specialCharsRule,
|
||||||
|
intl.formatMessage(messages.disallowedCharsError),
|
||||||
|
)
|
||||||
|
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
||||||
|
run: Yup.string()
|
||||||
|
.required(intl.formatMessage(messages.requiredFieldError))
|
||||||
|
.matches(
|
||||||
|
specialCharsRule,
|
||||||
|
intl.formatMessage(messages.disallowedCharsError),
|
||||||
|
)
|
||||||
|
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
values, errors, touched, handleChange, handleBlur, setFieldValue,
|
||||||
|
} = useFormik({
|
||||||
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
|
validateOnBlur: false,
|
||||||
|
validationSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (allowToCreateNewOrg) {
|
||||||
|
dispatch(fetchOrganizationsQuery());
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormFilled(Object.values(values).every((i) => i));
|
||||||
|
dispatch(updatePostErrors({}));
|
||||||
|
}, [values]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowErrorBanner(!!postErrors.errMsg);
|
||||||
|
}, [postErrors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (createOrRerunCourseSavingStatus === RequestStatus.SUCCESSFUL) {
|
||||||
|
dispatch(updateSavingStatus({ status: '' }));
|
||||||
|
const { url, destinationCourseKey } = redirectUrlObj;
|
||||||
|
// New courses' url to the outline page is provided in the url. However, for course
|
||||||
|
// re-runs the url is /course/. The actual destination for the rer-run's outline
|
||||||
|
// is in the destionationCourseKey attribute from the api.
|
||||||
|
if (url) {
|
||||||
|
if (destinationCourseKey) {
|
||||||
|
window.location.assign(`${getConfig().STUDIO_BASE_URL}${url}${destinationCourseKey}`);
|
||||||
|
} else {
|
||||||
|
window.location.assign(`${getConfig().STUDIO_BASE_URL}${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (createOrRerunCourseSavingStatus === RequestStatus.FAILED) {
|
||||||
|
dispatch(updateSavingStatus({ status: '' }));
|
||||||
|
}
|
||||||
|
}, [createOrRerunCourseSavingStatus]);
|
||||||
|
|
||||||
|
const hasErrorField = (fieldName) => !!errors[fieldName] && !!touched[fieldName];
|
||||||
|
const isFormInvalid = Object.keys(errors).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
intl,
|
||||||
|
errors,
|
||||||
|
values,
|
||||||
|
postErrors,
|
||||||
|
isFormFilled,
|
||||||
|
isFormInvalid,
|
||||||
|
organizations,
|
||||||
|
showErrorBanner,
|
||||||
|
dispatch,
|
||||||
|
handleBlur,
|
||||||
|
handleChange,
|
||||||
|
hasErrorField,
|
||||||
|
setFieldValue,
|
||||||
|
setShowErrorBanner,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export { useCreateOrRerunCourse };
|
||||||
2
src/generic/create-or-rerun-course/index.js
Normal file
2
src/generic/create-or-rerun-course/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as CreateOrRerunCourseForm } from './CreateOrRerunCourseForm';
|
||||||
|
export { useCreateOrRerunCourse } from './hooks';
|
||||||
130
src/generic/create-or-rerun-course/messages.js
Normal file
130
src/generic/create-or-rerun-course/messages.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
courseDisplayNameLabel: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.display-name.label',
|
||||||
|
defaultMessage: 'Course name',
|
||||||
|
},
|
||||||
|
courseDisplayNamePlaceholder: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.display-name.placeholder',
|
||||||
|
defaultMessage: 'e.g. Introduction to Computer Science',
|
||||||
|
},
|
||||||
|
courseDisplayNameCreateHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.create.display-name.help-text',
|
||||||
|
defaultMessage: 'The public display name for your course. This cannot be changed, but you can set a different display name in advanced settings later.',
|
||||||
|
},
|
||||||
|
courseDisplayNameRerunHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.rerun.display-name.help-text',
|
||||||
|
defaultMessage: 'The public display name for the new course. (This name is often the same as the original course name.)',
|
||||||
|
},
|
||||||
|
courseOrgLabel: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.org.label',
|
||||||
|
defaultMessage: 'Organization',
|
||||||
|
},
|
||||||
|
courseOrgPlaceholder: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.org.placeholder',
|
||||||
|
defaultMessage: 'e.g. UniversityX or OrganizationX',
|
||||||
|
},
|
||||||
|
courseOrgNoOptions: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.org.no-options',
|
||||||
|
defaultMessage: 'No options',
|
||||||
|
},
|
||||||
|
courseOrgCreateHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.create.org.help-text',
|
||||||
|
defaultMessage: 'The name of the organization sponsoring the course. {strong} This cannot be changed, but you can set a different display name in advanced settings later.',
|
||||||
|
},
|
||||||
|
courseOrgRerunHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.rerun.org.help-text',
|
||||||
|
defaultMessage: 'The name of the organization sponsoring the new course. (This name is often the same as the original organization name.) {strong}',
|
||||||
|
},
|
||||||
|
courseNoteNoSpaceAllowedStrong: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.no-space-allowed.strong',
|
||||||
|
defaultMessage: 'Note: No spaces or special characters are allowed.',
|
||||||
|
},
|
||||||
|
courseNoteOrgNameIsPartStrong: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.org.help-text.strong',
|
||||||
|
defaultMessage: 'Note: The organization name is part of the course URL.',
|
||||||
|
},
|
||||||
|
courseNumberLabel: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.number.label',
|
||||||
|
defaultMessage: 'Course number',
|
||||||
|
},
|
||||||
|
courseNumberPlaceholder: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.number.placeholder',
|
||||||
|
defaultMessage: 'e.g. CS101',
|
||||||
|
},
|
||||||
|
courseNumberCreateHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.create.number.help-text',
|
||||||
|
defaultMessage: 'The unique number that identifies your course within your organization. {strong}',
|
||||||
|
},
|
||||||
|
courseNumberRerunHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.rerun.number.help-text',
|
||||||
|
defaultMessage: 'The unique number that identifies the new course within the organization. (This number will be the same as the original course number and cannot be changed.)',
|
||||||
|
},
|
||||||
|
courseNotePartCourseURLRequireStrong: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.number.help-text.strong',
|
||||||
|
defaultMessage: 'Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.',
|
||||||
|
},
|
||||||
|
courseRunLabel: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.run.label',
|
||||||
|
defaultMessage: 'Course run',
|
||||||
|
},
|
||||||
|
courseRunPlaceholder: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.run.placeholder',
|
||||||
|
defaultMessage: 'e.g. 2014_T1',
|
||||||
|
},
|
||||||
|
courseRunCreateHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.create.run.help-text',
|
||||||
|
defaultMessage: 'The term in which your course will run. {strong}',
|
||||||
|
},
|
||||||
|
courseRunRerunHelpText: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.create.rerun.help-text',
|
||||||
|
defaultMessage: 'The term in which the new course will run. (This value is often different than the original course run value.){strong}',
|
||||||
|
},
|
||||||
|
defaultPlaceholder: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.default-placeholder',
|
||||||
|
defaultMessage: 'Label',
|
||||||
|
},
|
||||||
|
createButton: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.create.button.create',
|
||||||
|
defaultMessage: 'Create',
|
||||||
|
},
|
||||||
|
rerunCreateButton: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.rerun.button.create',
|
||||||
|
defaultMessage: 'Create re-run',
|
||||||
|
},
|
||||||
|
creatingButton: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.button.creating',
|
||||||
|
defaultMessage: 'Creating',
|
||||||
|
},
|
||||||
|
rerunningCreateButton: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.rerun.button.rerunning',
|
||||||
|
defaultMessage: 'Processing re-run request',
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.button.cancel',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
},
|
||||||
|
requiredFieldError: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.required.error',
|
||||||
|
defaultMessage: 'Required field.',
|
||||||
|
},
|
||||||
|
disallowedCharsError: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.disallowed-chars.error',
|
||||||
|
defaultMessage: 'Please do not use any spaces or special characters in this field.',
|
||||||
|
},
|
||||||
|
noSpaceError: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.no-space.error',
|
||||||
|
defaultMessage: 'Please do not use any spaces in this field.',
|
||||||
|
},
|
||||||
|
alertErrorExistsAriaLabelledBy: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.error.already-exists.labelledBy',
|
||||||
|
defaultMessage: 'alert-already-exists-title',
|
||||||
|
},
|
||||||
|
alertErrorExistsAriaDescribedBy: {
|
||||||
|
id: 'course-authoring.create-or-rerun-course.error.already-exists.aria.describedBy',
|
||||||
|
defaultMessage: 'alert-confirmation-description',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
44
src/generic/data/api.js
Normal file
44
src/generic/data/api.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
import { convertObjectToSnakeCase } from '../../utils';
|
||||||
|
|
||||||
|
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||||
|
export const getCreateOrRerunCourseUrl = () => new URL('course/', getApiBaseUrl()).href;
|
||||||
|
export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href;
|
||||||
|
export const getOrganizationsUrl = () => new URL('organizations', getApiBaseUrl()).href;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's organizations data.
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
export async function getOrganizations() {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().get(
|
||||||
|
getOrganizationsUrl(),
|
||||||
|
);
|
||||||
|
return camelCaseObject(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's course rerun data.
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
export async function getCourseRerun(courseId) {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().get(
|
||||||
|
getCourseRerunUrl(courseId),
|
||||||
|
);
|
||||||
|
return camelCaseObject(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or rerun course with data.
|
||||||
|
* @param {object} data
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
export async function createOrRerunCourse(courseData) {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().post(
|
||||||
|
getCreateOrRerunCourseUrl(),
|
||||||
|
convertObjectToSnakeCase(courseData, true),
|
||||||
|
);
|
||||||
|
return camelCaseObject(data);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user