Compare commits
21 Commits
saad/remov
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ec87c969 | ||
|
|
4835f72f2c | ||
|
|
3ab329d373 | ||
|
|
7c97ffecb5 | ||
|
|
90727590dd | ||
|
|
1c82a67364 | ||
|
|
d08ef83659 | ||
|
|
13bce7e034 | ||
|
|
54888d03bc | ||
|
|
e6d9f3a50d | ||
|
|
74b455287e | ||
|
|
e2adb45493 | ||
|
|
d4e9a6bec2 | ||
|
|
e6741496dc | ||
|
|
9304a83bef | ||
|
|
3173f41e63 | ||
|
|
866dd9bd31 | ||
|
|
f10ad9f525 | ||
|
|
81d78b9613 | ||
|
|
4886df7d6f | ||
|
|
62dfb75169 |
2
.env
2
.env
@@ -44,4 +44,4 @@ INVITE_STUDENTS_EMAIL_TO=''
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=''
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2"
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
|
||||
|
||||
@@ -47,4 +47,4 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2"
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
|
||||
|
||||
@@ -39,5 +39,4 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
# "other" is used to test the workflow for creating blocks that aren't supported by the built-in editors
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2,other"
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
|
||||
|
||||
13
.github/workflows/validate.yml
vendored
13
.github/workflows/validate.yml
vendored
@@ -9,17 +9,22 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: make validate.ci
|
||||
- name: Archive code coverage results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: code-coverage-report
|
||||
name: code-coverage-report-${{ matrix.node }}
|
||||
# When we're only using Node 20, replace the line above with the following:
|
||||
# name: code-coverage-report
|
||||
path: coverage/*.*
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -29,7 +34,9 @@ jobs:
|
||||
- name: Download code coverage results
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: code-coverage-report
|
||||
name: code-coverage-report-20
|
||||
# When we're only using Node 20, replace the line above with the following:
|
||||
# name: code-coverage-report
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
.idea
|
||||
.run
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
|
||||
@@ -85,8 +85,8 @@ Troubleshooting
|
||||
---------------
|
||||
|
||||
* If you see an "Invalid Host header" error, then you're probably using a different domain name for your devstack such as
|
||||
``local.edly.io`` or ``local.overhang.io`` (not the new recommended default, ``local.openedx.io``). In that case, run
|
||||
these commands to update your devstack's domain names:
|
||||
``local.edly.io`` or ``local.overhang.io`` (not the new recommended default, ``local.openedx.io``). In that case, run
|
||||
these commands to update your devstack's domain names:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -98,7 +98,7 @@ Troubleshooting
|
||||
* If tutor-mfe is not starting the authoring MFE in development mode (eg. `tutor dev start authoring` fails), it may be due to
|
||||
using a tutor version that expects the MFE name to be frontend-app-course-authoring (the previous name of this repo). To fix
|
||||
this, you can rename the cloned repo directory to frontend-app-course-authoring. More information can be found in
|
||||
`this forum post <https://discuss.openedx.org/t/repo-rename-frontend-app-course-authoring-frontend-app-authoring/13930/2>`__.
|
||||
[this forum post](https://discuss.openedx.org/t/repo-rename-frontend-app-course-authoring-frontend-app-authoring/13930/2)
|
||||
|
||||
|
||||
Features
|
||||
|
||||
@@ -12,7 +12,6 @@ metadata:
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-tnl
|
||||
type: 'website'
|
||||
|
||||
11
openedx.yaml
Normal file
11
openedx.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
nick: cath
|
||||
oeps: {}
|
||||
owner: edx/platform-core-tnl
|
||||
openedx-release:
|
||||
# The openedx-release key is described in OEP-10:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
|
||||
# The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
|
||||
ref: master
|
||||
146
package-lock.json
generated
146
package-lock.json
generated
@@ -21,7 +21,7 @@
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-component-footer": "^14.1.0",
|
||||
"@edx/frontend-component-header": "^5.8.3",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||
"@edx/frontend-platform": "^8.0.3",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
@@ -35,7 +35,7 @@
|
||||
"@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams",
|
||||
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
|
||||
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
|
||||
"@openedx/frontend-build": "^14.2.0",
|
||||
"@openedx/frontend-build": "^14.0.14",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.1",
|
||||
"@openedx/paragon": "^22.8.1",
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
@@ -2136,9 +2136,10 @@
|
||||
"license": "AGPL-3.0"
|
||||
},
|
||||
"node_modules/@edx/eslint-config": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/eslint-config/-/eslint-config-4.3.0.tgz",
|
||||
"integrity": "sha512-4W9wFG4ALr3xocakCsncgJbK67RHfSmDwHDXKHReFtjxl/FRkxhS6qayz189oChqfANieeV3zRCLaq44bLf+/A==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/eslint-config/-/eslint-config-4.2.0.tgz",
|
||||
"integrity": "sha512-2wuIw49uyj6gRwS74qJ8WhBU+X2FOP4uot40sthIC4YU9qCM7WJOcOuAhkRPP1FvZKd3UQH3gZM7eJ85xzDBqA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
@@ -2175,10 +2176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.8.3.tgz",
|
||||
"integrity": "sha512-cPenGEirOE7sQwyjakK9Vy/oyvBy4gzhCkf5pxiYYuOhTe0uK/iLnnThJh7R0XdkGdxMMOvUBJVmV4TGC1BHJQ==",
|
||||
"license": "AGPL-3.0",
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.6.0.tgz",
|
||||
"integrity": "sha512-ITLLrej6BbWVc/0baMkKg/ACTvUGSR188Rn/BC2Y82Tdu8gRsZB6+0GUsDX/6FJjeIazLXdUusKlfwVU90sXLA==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.6.0",
|
||||
@@ -2198,8 +2198,7 @@
|
||||
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.9.0 || ^17.0.0",
|
||||
"react-dom": "^16.9.0 || ^17.0.0",
|
||||
"react-router-dom": "^6.14.2"
|
||||
"react-dom": "^16.9.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/react-responsive": {
|
||||
@@ -3555,9 +3554,10 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@openedx/frontend-build": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.2.0.tgz",
|
||||
"integrity": "sha512-yeBanEnfpYY3gci9vBmPlFZLzTIhbUVjM8DxldPeaHzG7IZzMVR14KNkS470siCpikNxarordg3fGXSDU+QYHA==",
|
||||
"version": "14.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.1.4.tgz",
|
||||
"integrity": "sha512-DMzkitHqemtqwxmDsF8y7zRVAJcW8URPfWcLKtFvXffqJ3WW7fJXXMmiZWKra/vGBw3SRyYRqvdzQG1d2giPAw==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.24.8",
|
||||
"@babel/core": "7.24.9",
|
||||
@@ -3567,7 +3567,7 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.24.8",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@edx/eslint-config": "^4.3.0",
|
||||
"@edx/eslint-config": "4.2.0",
|
||||
"@edx/new-relic-source-map-webpack-plugin": "2.1.0",
|
||||
"@edx/typescript-config": "1.1.0",
|
||||
"@formatjs/cli": "^6.0.3",
|
||||
@@ -5168,8 +5168,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
@@ -5201,8 +5200,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -5212,8 +5210,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
@@ -5238,8 +5235,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
|
||||
"integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
"@typescript-eslint/visitor-keys": "5.62.0"
|
||||
@@ -5254,8 +5250,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
|
||||
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
||||
"@typescript-eslint/utils": "5.62.0",
|
||||
@@ -5280,8 +5275,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
|
||||
"integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
@@ -5292,8 +5286,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
|
||||
"integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
"@typescript-eslint/visitor-keys": "5.62.0",
|
||||
@@ -5318,8 +5311,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -5329,8 +5321,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
|
||||
"integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
@@ -5354,8 +5345,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -5365,8 +5355,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
|
||||
"integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
@@ -5381,8 +5370,7 @@
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
@@ -5900,8 +5888,7 @@
|
||||
},
|
||||
"node_modules/array.prototype.tosorted": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
|
||||
"integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
@@ -5947,8 +5934,7 @@
|
||||
},
|
||||
"node_modules/ast-types-flow": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
|
||||
"integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag=="
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/astral-regex": {
|
||||
"version": "2.0.0",
|
||||
@@ -6031,9 +6017,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz",
|
||||
"integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==",
|
||||
"version": "4.10.0",
|
||||
"license": "MPL-2.0",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -6061,8 +6046,7 @@
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.4.tgz",
|
||||
"integrity": "sha512-aPTElBrbifBU1krmZxGZOlBkslORe7Ll7+BDnI50Wy4LgOt69luMgevkDfTq1O/ZgprooPCtWpjCwKSZw/iZ4A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -6644,9 +6628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001690",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
||||
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
|
||||
"version": "1.0.30001667",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
|
||||
"integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7018,8 +7002,7 @@
|
||||
},
|
||||
"node_modules/confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
||||
"integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/connect-history-api-fallback": {
|
||||
"version": "2.0.0",
|
||||
@@ -7429,8 +7412,7 @@
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "3.0.2",
|
||||
@@ -8377,8 +8359,7 @@
|
||||
},
|
||||
"node_modules/eslint-config-airbnb": {
|
||||
"version": "19.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz",
|
||||
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"object.assign": "^4.1.2",
|
||||
@@ -8397,8 +8378,7 @@
|
||||
},
|
||||
"node_modules/eslint-config-airbnb-base": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz",
|
||||
"integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confusing-browser-globals": "^1.0.10",
|
||||
"object.assign": "^4.1.2",
|
||||
@@ -8415,8 +8395,7 @@
|
||||
},
|
||||
"node_modules/eslint-config-airbnb-typescript": {
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz",
|
||||
"integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-config-airbnb-base": "^15.0.0"
|
||||
},
|
||||
@@ -8745,8 +8724,7 @@
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz",
|
||||
"integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.7",
|
||||
"aria-query": "^5.1.3",
|
||||
@@ -8774,13 +8752,11 @@
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.32.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
|
||||
"integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.6",
|
||||
"array.prototype.flatmap": "^1.3.1",
|
||||
@@ -8807,8 +8783,7 @@
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
|
||||
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -8818,8 +8793,7 @@
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
@@ -8829,8 +8803,7 @@
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/resolve": {
|
||||
"version": "2.0.0-next.5",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||
"integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.13.0",
|
||||
"path-parse": "^1.0.7",
|
||||
@@ -12325,8 +12298,7 @@
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
"integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.6",
|
||||
"array.prototype.flat": "^1.3.1",
|
||||
@@ -12379,13 +12351,11 @@
|
||||
},
|
||||
"node_modules/language-subtag-registry": {
|
||||
"version": "0.3.23",
|
||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
|
||||
"integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/language-tags": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
|
||||
"integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"language-subtag-registry": "~0.3.2"
|
||||
}
|
||||
@@ -13017,8 +12987,7 @@
|
||||
},
|
||||
"node_modules/natural-compare-lite": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
|
||||
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
@@ -15581,8 +15550,7 @@
|
||||
},
|
||||
"node_modules/object.entries": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
|
||||
"integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
@@ -15623,8 +15591,7 @@
|
||||
},
|
||||
"node_modules/object.hasown": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz",
|
||||
"integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-properties": "^1.2.1",
|
||||
"es-abstract": "^1.23.2",
|
||||
@@ -18967,8 +18934,7 @@
|
||||
},
|
||||
"node_modules/string.prototype.matchall": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
|
||||
"integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1",
|
||||
@@ -19855,8 +19821,7 @@
|
||||
},
|
||||
"node_modules/tsutils": {
|
||||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
|
||||
"integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.8.1"
|
||||
},
|
||||
@@ -19869,8 +19834,7 @@
|
||||
},
|
||||
"node_modules/tsutils/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-component-footer": "^14.1.0",
|
||||
"@edx/frontend-component-header": "^5.8.3",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||
"@edx/frontend-platform": "^8.0.3",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
@@ -64,7 +64,7 @@
|
||||
"@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams",
|
||||
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
|
||||
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
|
||||
"@openedx/frontend-build": "^14.2.0",
|
||||
"@openedx/frontend-build": "^14.0.14",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.1",
|
||||
"@openedx/paragon": "^22.8.1",
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { bbbPlanTypes } from '../constants';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { GroupTypes } from 'CourseAuthoring/data/constants';
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import Header from './header';
|
||||
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
|
||||
import { fetchCourseDetail } from './data/thunks';
|
||||
import { useModel } from './generic/model-store';
|
||||
import NotFoundAlert from './generic/NotFoundAlert';
|
||||
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
||||
import { fetchStudioHomeData } from './studio-home/data/thunks';
|
||||
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
|
||||
import { RequestStatus } from './data/constants';
|
||||
import Loading from './generic/Loading';
|
||||
@@ -20,9 +21,12 @@ const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseDetail(courseId));
|
||||
dispatch(fetchWaffleFlags(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStudioHomeData());
|
||||
}, []);
|
||||
|
||||
const courseDetail = useModel('courseDetails', courseId);
|
||||
|
||||
const courseNumber = courseDetail ? courseDetail.number : null;
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import React from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import initializeStore from './store';
|
||||
import CourseAuthoringPage from './CourseAuthoringPage';
|
||||
import PagesAndResources from './pages-and-resources/PagesAndResources';
|
||||
import { executeThunk } from './utils';
|
||||
import { fetchCourseApps } from './pages-and-resources/data/thunks';
|
||||
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
|
||||
import { getApiWaffleFlagsUrl } from './data/api';
|
||||
import { initializeMocks, render } from './testUtils';
|
||||
import { fetchCourseDetail } from './data/thunks';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
let mockPathname = '/evilguy/';
|
||||
@@ -19,14 +25,17 @@ jest.mock('react-router-dom', () => ({
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mocks = initializeMocks();
|
||||
store = mocks.reduxStore;
|
||||
axiosMock = mocks.axiosMock;
|
||||
axiosMock
|
||||
.onGet(getApiWaffleFlagsUrl(courseId))
|
||||
.reply(200, {});
|
||||
await executeThunk(fetchWaffleFlags(courseId), store.dispatch);
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
describe('Editor Pages Load no header', () => {
|
||||
@@ -42,9 +51,13 @@ describe('Editor Pages Load no header', () => {
|
||||
mockPathname = '/editor/';
|
||||
await mockStoreSuccess();
|
||||
const wrapper = render(
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
</CourseAuthoringPage>
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
</CourseAuthoringPage>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
,
|
||||
);
|
||||
expect(wrapper.queryByRole('status')).not.toBeInTheDocument();
|
||||
@@ -53,9 +66,13 @@ describe('Editor Pages Load no header', () => {
|
||||
mockPathname = '/evilguy/';
|
||||
await mockStoreSuccess();
|
||||
const wrapper = render(
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
</CourseAuthoringPage>
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
</CourseAuthoringPage>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
,
|
||||
);
|
||||
expect(wrapper.queryByRole('status')).toBeInTheDocument();
|
||||
@@ -83,7 +100,14 @@ describe('Course authoring page', () => {
|
||||
};
|
||||
test('renders not found page on non-existent course key', async () => {
|
||||
await mockStoreNotFound();
|
||||
const wrapper = render(<CourseAuthoringPage courseId={courseId} />);
|
||||
const wrapper = render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseAuthoringPage courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
,
|
||||
);
|
||||
expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument();
|
||||
});
|
||||
test('does not render not found page on other kinds of error', async () => {
|
||||
@@ -94,9 +118,13 @@ describe('Course authoring page', () => {
|
||||
// found alert is not present.
|
||||
const contentTestId = 'courseAuthoringPageContent';
|
||||
const wrapper = render(
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<div data-testid={contentTestId} />
|
||||
</CourseAuthoringPage>
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<div data-testid={contentTestId} />
|
||||
</CourseAuthoringPage>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
,
|
||||
);
|
||||
expect(await wrapper.findByTestId(contentTestId)).toBeInTheDocument();
|
||||
|
||||
@@ -17,15 +17,13 @@ import ScheduleAndDetails from './schedule-and-details';
|
||||
import { GradingSettings } from './grading-settings';
|
||||
import CourseTeam from './course-team/CourseTeam';
|
||||
import { CourseUpdates } from './course-updates';
|
||||
import { CourseUnit, IframeProvider } from './course-unit';
|
||||
import { CourseUnit } from './course-unit';
|
||||
import { Certificates } from './certificates';
|
||||
import CourseExportPage from './export-page/CourseExportPage';
|
||||
import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage';
|
||||
import CourseImportPage from './import-page/CourseImportPage';
|
||||
import { DECODED_ROUTES } from './constants';
|
||||
import CourseChecklist from './course-checklist';
|
||||
import GroupConfigurations from './group-configurations';
|
||||
import CourseLibraries from './course-libraries';
|
||||
|
||||
/**
|
||||
* As of this writing, these routes are mounted at a path prefixed with the following:
|
||||
@@ -57,10 +55,6 @@ const CourseAuthoringRoutes = () => {
|
||||
path="course_info"
|
||||
element={<PageWrap><CourseUpdates courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="libraries"
|
||||
element={<PageWrap><CourseLibraries courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="assets"
|
||||
element={<PageWrap><FilesPage courseId={courseId} /></PageWrap>}
|
||||
@@ -85,7 +79,7 @@ const CourseAuthoringRoutes = () => {
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<PageWrap><IframeProvider><CourseUnit courseId={courseId} /></IframeProvider></PageWrap>}
|
||||
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
@@ -124,10 +118,6 @@ const CourseAuthoringRoutes = () => {
|
||||
path="export"
|
||||
element={<PageWrap><CourseExportPage courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="optimizer"
|
||||
element={<PageWrap><CourseOptimizerPage courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="checklists"
|
||||
element={<PageWrap><CourseChecklist courseId={courseId} /></PageWrap>}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
|
||||
import { executeThunk } from './utils';
|
||||
import { getApiWaffleFlagsUrl } from './data/api';
|
||||
import { fetchWaffleFlags } from './data/thunks';
|
||||
import {
|
||||
screen, initializeMocks, render, waitFor,
|
||||
} from './testUtils';
|
||||
import initializeStore from './store';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const pagesAndResourcesMockText = 'Pages And Resources';
|
||||
@@ -50,59 +50,68 @@ jest.mock('./custom-pages/CustomPages', () => (props) => {
|
||||
});
|
||||
|
||||
describe('<CourseAuthoringRoutes>', () => {
|
||||
beforeEach(async () => {
|
||||
const { axiosMock, reduxStore } = initializeMocks();
|
||||
store = reduxStore;
|
||||
axiosMock
|
||||
.onGet(getApiWaffleFlagsUrl(courseId))
|
||||
.reply(200, {});
|
||||
await executeThunk(fetchWaffleFlags(courseId), store.dispatch);
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('renders the PagesAndResources component when the pages and resources route is active', async () => {
|
||||
fit('renders the PagesAndResources component when the pages and resources route is active', () => {
|
||||
render(
|
||||
<CourseAuthoringRoutes />,
|
||||
{ routerProps: { initialEntries: ['/pages-and-resources'] } },
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={['/pages-and-resources']}>
|
||||
<CourseAuthoringRoutes />
|
||||
</MemoryRouter>
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
courseId,
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(pagesAndResourcesMockText)).toBeVisible();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
courseId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the EditorContainer component when the course editor route is active', async () => {
|
||||
it('renders the EditorContainer component when the course editor route is active', () => {
|
||||
render(
|
||||
<CourseAuthoringRoutes />,
|
||||
{ routerProps: { initialEntries: ['/editor/video/block-id'] } },
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={['/editor/video/block-id']}>
|
||||
<CourseAuthoringRoutes />
|
||||
</MemoryRouter>
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(editorContainerMockText)).toBeInTheDocument();
|
||||
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
courseId,
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(editorContainerMockText)).toBeInTheDocument();
|
||||
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
learningContextId: courseId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the VideoSelectorContainer component when the course videos route is active', async () => {
|
||||
it('renders the VideoSelectorContainer component when the course videos route is active', () => {
|
||||
render(
|
||||
<CourseAuthoringRoutes />,
|
||||
{ routerProps: { initialEntries: ['/editor/course-videos/block-id'] } },
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={['/editor/course-videos/block-id']}>
|
||||
<CourseAuthoringRoutes />
|
||||
</MemoryRouter>
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
|
||||
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
courseId,
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(videoSelectorContainerMockText)).toBeInTheDocument();
|
||||
expect(screen.queryByText(pagesAndResourcesMockText)).not.toBeInTheDocument();
|
||||
expect(mockComponentFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
courseId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as advancedSettingsMock } from './advancedSettings';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { convertObjectToSnakeCase } from '../../utils';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as AdvancedSettings } from './AdvancedSettings';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const defaultCertificate = {
|
||||
courseTitle: '',
|
||||
signatories: [{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as Certificates } from './Certificates';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getSidebarData = ({ messages, intl }) => [
|
||||
{
|
||||
title: intl.formatMessage(messages.workingWithCertificatesTitle),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { convertObjectToSnakeCase } from '../utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const prepareCertificatePayload = (data) => convertObjectToSnakeCase(({
|
||||
...data,
|
||||
courseTitle: data.courseTitle,
|
||||
|
||||
@@ -27,8 +27,6 @@ export const NOTIFICATION_MESSAGES = {
|
||||
copying: 'Copying',
|
||||
pasting: 'Pasting',
|
||||
discardChanges: 'Discarding changes',
|
||||
moving: 'Moving',
|
||||
undoMoving: 'Undo moving',
|
||||
publishing: 'Publishing',
|
||||
hidingFromStudents: 'Hiding from students',
|
||||
makingVisibleToStudents: 'Making visible to students',
|
||||
@@ -58,7 +56,6 @@ export const COURSE_BLOCK_NAMES = ({
|
||||
chapter: { id: 'chapter', name: 'Section' },
|
||||
sequential: { id: 'sequential', name: 'Subsection' },
|
||||
vertical: { id: 'vertical', name: 'Unit' },
|
||||
libraryContent: { id: 'library_content', name: 'Library content' },
|
||||
component: { id: 'component', name: 'Component' },
|
||||
});
|
||||
|
||||
@@ -77,17 +74,3 @@ export const REGEX_RULES = {
|
||||
specialCharsRule: /^[a-zA-Z0-9_\-.'*~\s]+$/,
|
||||
noSpaceRule: /^\S*$/,
|
||||
};
|
||||
|
||||
/**
|
||||
* Feature policy for iframe, allowing access to certain courseware-related media.
|
||||
*
|
||||
* We must use the wildcard (*) origin for each feature, as courseware content
|
||||
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
|
||||
* block that iframes external course content.
|
||||
|
||||
* This policy was selected in conference with the edX Security Working Group.
|
||||
* Changes to it should be vetted by them (security@edx.org).
|
||||
*/
|
||||
export const IFRAME_FEATURE_POLICY = (
|
||||
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *'
|
||||
);
|
||||
|
||||
@@ -25,9 +25,9 @@ import TagsTree from './TagsTree';
|
||||
import { ContentTagsDrawerContext } from './common/context';
|
||||
|
||||
/** @typedef {import("./ContentTagsCollapsible").TaxonomySelectProps} TaxonomySelectProps */
|
||||
/** @typedef {import("../taxonomy/data/types.js").TaxonomyData} TaxonomyData */
|
||||
/** @typedef {import("./data/types.js").Tag} ContentTagData */
|
||||
/** @typedef {import("./data/types.js").StagedTagData} StagedTagData */
|
||||
/** @typedef {import("../taxonomy/data/types.mjs").TaxonomyData} TaxonomyData */
|
||||
/** @typedef {import("./data/types.mjs").Tag} ContentTagData */
|
||||
/** @typedef {import("./data/types.mjs").StagedTagData} StagedTagData */
|
||||
|
||||
/**
|
||||
* Custom Menu component for our Select box
|
||||
|
||||
@@ -6,11 +6,11 @@ import { cloneDeep } from 'lodash';
|
||||
import { useContentTaxonomyTagsUpdater } from './data/apiHooks';
|
||||
import { ContentTagsDrawerContext } from './common/context';
|
||||
|
||||
/** @typedef {import("../taxonomy/data/types.js").TaxonomyData} TaxonomyData */
|
||||
/** @typedef {import("./data/types.js").Tag} ContentTagData */
|
||||
/** @typedef {import("../taxonomy/data/types.mjs").TaxonomyData} TaxonomyData */
|
||||
/** @typedef {import("./data/types.mjs").Tag} ContentTagData */
|
||||
/** @typedef {import("./ContentTagsCollapsible").TagTreeEntry} TagTreeEntry */
|
||||
/** @typedef {import("./data/types.js").StagedTagData} StagedTagData */
|
||||
/** @typedef {import("./data/types.js").UpdateTagsData} UpdateTagsData */
|
||||
/** @typedef {import("./data/types.mjs").StagedTagData} StagedTagData */
|
||||
/** @typedef {import("./data/types.mjs").UpdateTagsData} UpdateTagsData */
|
||||
|
||||
/**
|
||||
* Util function that sorts the keys of a tree in alphabetical order.
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from './data/api.mocks';
|
||||
import { getContentTaxonomyTagsApiUrl } from './data/api';
|
||||
|
||||
const path = '/content/:contentId?/*';
|
||||
const path = '/content/:contentId/*';
|
||||
const mockOnClose = jest.fn();
|
||||
const mockSetBlockingSheet = jest.fn();
|
||||
const mockNavigate = jest.fn();
|
||||
|
||||
@@ -12,7 +12,7 @@ import classNames from 'classnames';
|
||||
import messages from './messages';
|
||||
import ContentTagsCollapsible from './ContentTagsCollapsible';
|
||||
import Loading from '../generic/Loading';
|
||||
import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper';
|
||||
import useContentTagsDrawerContext from './ContentTagsDrawerHelper';
|
||||
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
|
||||
|
||||
interface TaxonomyListProps {
|
||||
@@ -246,7 +246,7 @@ const ContentTagsDrawer = ({
|
||||
throw new Error('Error: contentId cannot be null.');
|
||||
}
|
||||
|
||||
const context = useCreateContentTagsDrawerContext(contentId, !readOnly);
|
||||
const context = useContentTagsDrawerContext(contentId, !readOnly);
|
||||
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
|
||||
|
||||
const {
|
||||
|
||||
@@ -8,21 +8,46 @@ import { extractOrgFromContentId, languageExportId } from './utils';
|
||||
import messages from './messages';
|
||||
import { ContentTagsDrawerSheetContext } from './common/context';
|
||||
|
||||
/** @typedef {import("./data/types.js").Tag} ContentTagData */
|
||||
/** @typedef {import("./data/types.js").StagedTagData} StagedTagData */
|
||||
/** @typedef {import("./data/types.js").TagsInTaxonomy} TagsInTaxonomy */
|
||||
/** @typedef {import("./common/context").ContentTagsDrawerContextData} ContentTagsDrawerContextData */
|
||||
/** @typedef {import("./data/types.mjs").Tag} ContentTagData */
|
||||
/** @typedef {import("./data/types.mjs").StagedTagData} StagedTagData */
|
||||
/** @typedef {import("./data/types.mjs").TagsInTaxonomy} TagsInTaxonomy */
|
||||
|
||||
/**
|
||||
* Helper hook for *creating* a `ContentTagsDrawerContext`.
|
||||
* Handles the context and all the underlying logic for the ContentTagsDrawer component.
|
||||
*
|
||||
* To *use* the context, just use `useContext(ContentTagsDrawerContext)`
|
||||
* Handles the context and all the underlying logic for the ContentTagsDrawer component
|
||||
* @param {string} contentId
|
||||
* @param {boolean} canTagObject
|
||||
* @returns {ContentTagsDrawerContextData}
|
||||
* @returns {{
|
||||
* stagedContentTags: Record<number, StagedTagData[]>,
|
||||
* addStagedContentTag: (taxonomyId: number, addedTag: StagedTagData) => void,
|
||||
* removeStagedContentTag: (taxonomyId: number, tagValue: string) => void,
|
||||
* removeGlobalStagedContentTag: (taxonomyId: number, tagValue: string) => void,
|
||||
* addRemovedContentTag: (taxonomyId: number, tagValue: string) => void,
|
||||
* deleteRemovedContentTag: (taxonomyId: number, tagValue: string) => void,
|
||||
* setStagedTags: (taxonomyId: number, tagsList: StagedTagData[]) => void,
|
||||
* globalStagedContentTags: Record<number, StagedTagData[]>,
|
||||
* globalStagedRemovedContentTags: Record<number, string>,
|
||||
* setGlobalStagedContentTags: Function,
|
||||
* commitGlobalStagedTags: () => void,
|
||||
* commitGlobalStagedTagsStatus: string,
|
||||
* isContentDataLoaded: boolean,
|
||||
* isContentTaxonomyTagsLoaded: boolean,
|
||||
* isTaxonomyListLoaded: boolean,
|
||||
* contentName: string,
|
||||
* tagsByTaxonomy: TagsInTaxonomy[],
|
||||
* isEditMode: boolean,
|
||||
* toEditMode: () => void,
|
||||
* toReadMode: () => void,
|
||||
* collapsibleStates: Record<number, boolean>,
|
||||
* openCollapsible: (taxonomyId: number) => void,
|
||||
* closeCollapsible: (taxonomyId: number) => void,
|
||||
* toastMessage: string | undefined,
|
||||
* showToastAfterSave: () => void,
|
||||
* closeToast: () => void,
|
||||
* setCollapsibleToInitalState: () => void,
|
||||
* otherTaxonomies: TagsInTaxonomy[],
|
||||
* }}
|
||||
*/
|
||||
export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
|
||||
const useContentTagsDrawerContext = (contentId, canTagObject) => {
|
||||
const intl = useIntl();
|
||||
const org = extractOrgFromContentId(contentId);
|
||||
|
||||
@@ -440,3 +465,5 @@ export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
|
||||
otherTaxonomies,
|
||||
};
|
||||
};
|
||||
|
||||
export default useContentTagsDrawerContext;
|
||||
|
||||
49
src/content-tags-drawer/common/context.js
Normal file
49
src/content-tags-drawer/common/context.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @ts-check
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import React from 'react';
|
||||
|
||||
/** @typedef {import("../data/types.mjs").TagsInTaxonomy} TagsInTaxonomy */
|
||||
/** @typedef {import("../data/types.mjs").StagedTagData} StagedTagData */
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const ContentTagsDrawerContext = React.createContext({
|
||||
stagedContentTags: /** @type{Record<number, StagedTagData[]>} */ ({}),
|
||||
globalStagedContentTags: /** @type{Record<number, StagedTagData[]>} */ ({}),
|
||||
globalStagedRemovedContentTags: /** @type{Record<number, string>} */ ({}),
|
||||
addStagedContentTag: /** @type{(taxonomyId: number, addedTag: StagedTagData) => void} */ (() => {}),
|
||||
removeStagedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
|
||||
removeGlobalStagedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
|
||||
addRemovedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
|
||||
deleteRemovedContentTag: /** @type{(taxonomyId: number, tagValue: string) => void} */ (() => {}),
|
||||
setStagedTags: /** @type{(taxonomyId: number, tagsList: StagedTagData[]) => void} */ (() => {}),
|
||||
setGlobalStagedContentTags: /** @type{Function} */ (() => {}),
|
||||
commitGlobalStagedTags: /** @type{() => void} */ (() => {}),
|
||||
commitGlobalStagedTagsStatus: /** @type{null|string} */ (null),
|
||||
isContentDataLoaded: /** @type{boolean} */ (false),
|
||||
isContentTaxonomyTagsLoaded: /** @type{boolean} */ (false),
|
||||
isTaxonomyListLoaded: /** @type{boolean} */ (false),
|
||||
contentName: /** @type{string} */ (''),
|
||||
tagsByTaxonomy: /** @type{TagsInTaxonomy[]} */ ([]),
|
||||
isEditMode: /** @type{boolean} */ (false),
|
||||
toEditMode: /** @type{() => void} */ (() => {}),
|
||||
toReadMode: /** @type{() => void} */ (() => {}),
|
||||
collapsibleStates: /** @type{Record<number, boolean>} */ ({}),
|
||||
openCollapsible: /** @type{(taxonomyId: number) => void} */ (() => {}),
|
||||
closeCollapsible: /** @type{(taxonomyId: number) => void} */ (() => {}),
|
||||
toastMessage: /** @type{string|undefined} */ (undefined),
|
||||
showToastAfterSave: /** @type{() => void} */ (() => {}),
|
||||
closeToast: /** @type{() => void} */ (() => {}),
|
||||
setCollapsibleToInitalState: /** @type{() => void} */ (() => {}),
|
||||
otherTaxonomies: /** @type{TagsInTaxonomy[]} */ ([]),
|
||||
});
|
||||
|
||||
// This context has not been added to ContentTagsDrawerContext because it has been
|
||||
// created one level higher to control the behavior of the Sheet that contatins the Drawer.
|
||||
// This logic is not used in legacy edx-platform screens. But it can be separated if we keep
|
||||
// the contexts separate.
|
||||
// TODO We can join both contexts when the Drawer is no longer used on edx-platform
|
||||
/* istanbul ignore next */
|
||||
export const ContentTagsDrawerSheetContext = React.createContext({
|
||||
blockingSheet: /** @type{boolean} */ (false),
|
||||
setBlockingSheet: /** @type{Function} */ (() => {}),
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { TagsInTaxonomy, StagedTagData } from '../data/types';
|
||||
|
||||
export interface ContentTagsDrawerContextData {
|
||||
stagedContentTags: Record<number, StagedTagData[]>;
|
||||
globalStagedContentTags: Record<number, StagedTagData[]>;
|
||||
globalStagedRemovedContentTags: Record<number, string>;
|
||||
addStagedContentTag: (taxonomyId: number, addedTag: StagedTagData) => void;
|
||||
removeStagedContentTag: (taxonomyId: number, tagValue: string) => void;
|
||||
removeGlobalStagedContentTag: (taxonomyId: number, tagValue: string) => void;
|
||||
addRemovedContentTag: (taxonomyId: number, tagValue: string) => void;
|
||||
deleteRemovedContentTag: (taxonomyId: number, tagValue: string) => void;
|
||||
setStagedTags: (taxonomyId: number, tagsList: StagedTagData[]) => void;
|
||||
setGlobalStagedContentTags: Function;
|
||||
commitGlobalStagedTags: () => void;
|
||||
commitGlobalStagedTagsStatus: null | string;
|
||||
isContentDataLoaded: boolean;
|
||||
isContentTaxonomyTagsLoaded: boolean;
|
||||
isTaxonomyListLoaded: boolean;
|
||||
contentName: string;
|
||||
tagsByTaxonomy: TagsInTaxonomy[];
|
||||
isEditMode: boolean;
|
||||
toEditMode: () => void;
|
||||
toReadMode: () => void;
|
||||
collapsibleStates: Record<number, boolean>;
|
||||
openCollapsible: (taxonomyId: number) => void;
|
||||
closeCollapsible: (taxonomyId: number) => void;
|
||||
toastMessage: string | undefined;
|
||||
showToastAfterSave: () => void;
|
||||
closeToast: () => void;
|
||||
setCollapsibleToInitalState: () => void;
|
||||
otherTaxonomies: TagsInTaxonomy[];
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const ContentTagsDrawerContext = React.createContext<ContentTagsDrawerContextData>({
|
||||
stagedContentTags: {},
|
||||
globalStagedContentTags: {},
|
||||
globalStagedRemovedContentTags: {},
|
||||
addStagedContentTag: () => {},
|
||||
removeStagedContentTag: () => {},
|
||||
removeGlobalStagedContentTag: () => {},
|
||||
addRemovedContentTag: () => {},
|
||||
deleteRemovedContentTag: () => {},
|
||||
setStagedTags: () => {},
|
||||
setGlobalStagedContentTags: () => {},
|
||||
commitGlobalStagedTags: () => {},
|
||||
commitGlobalStagedTagsStatus: null,
|
||||
isContentDataLoaded: false,
|
||||
isContentTaxonomyTagsLoaded: false,
|
||||
isTaxonomyListLoaded: false,
|
||||
contentName: '',
|
||||
tagsByTaxonomy: [],
|
||||
isEditMode: false,
|
||||
toEditMode: () => {},
|
||||
toReadMode: () => {},
|
||||
collapsibleStates: {},
|
||||
openCollapsible: () => {},
|
||||
closeCollapsible: () => {},
|
||||
toastMessage: undefined,
|
||||
showToastAfterSave: () => {},
|
||||
closeToast: () => {},
|
||||
setCollapsibleToInitalState: () => {},
|
||||
otherTaxonomies: [],
|
||||
});
|
||||
|
||||
// This context has not been added to ContentTagsDrawerContext because it has been
|
||||
// created one level higher to control the behavior of the Sheet that contatins the Drawer.
|
||||
// This logic is not used in legacy edx-platform screens. But it can be separated if we keep
|
||||
// the contexts separate.
|
||||
// TODO We can join both contexts when the Drawer is no longer used on edx-platform
|
||||
/* istanbul ignore next */
|
||||
export const ContentTagsDrawerSheetContext = React.createContext({
|
||||
blockingSheet: false,
|
||||
setBlockingSheet: (() => {}) as (blockingSheet: boolean) => void,
|
||||
});
|
||||
@@ -38,7 +38,7 @@ export const getContentTaxonomyTagsCountApiUrl = (contentId) => new URL(`api/con
|
||||
* Get all tags that belong to taxonomy.
|
||||
* @param {number} taxonomyId The id of the taxonomy to fetch tags for
|
||||
* @param {{page?: number, searchTerm?: string, parentTag?: string}} options
|
||||
* @returns {Promise<import("../../taxonomy/data/types.js").TagListData>}
|
||||
* @returns {Promise<import("../../taxonomy/tag-list/data/types.mjs").TagListData>}
|
||||
*/
|
||||
export async function getTaxonomyTagsData(taxonomyId, options = {}) {
|
||||
const url = getTaxonomyTagsApiUrl(taxonomyId, options);
|
||||
@@ -49,7 +49,7 @@ export async function getTaxonomyTagsData(taxonomyId, options = {}) {
|
||||
/**
|
||||
* Get the tags that are applied to the content object
|
||||
* @param {string} contentId The id of the content object to fetch the applied tags for
|
||||
* @returns {Promise<import("./types.js").ContentTaxonomyTagsData>}
|
||||
* @returns {Promise<import("./types.mjs").ContentTaxonomyTagsData>}
|
||||
*/
|
||||
export async function getContentTaxonomyTagsData(contentId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getContentTaxonomyTagsApiUrl(contentId));
|
||||
@@ -72,7 +72,7 @@ export async function getContentTaxonomyTagsCount(contentId) {
|
||||
/**
|
||||
* Fetch meta data (eg: display_name) about the content object (unit/compoenent)
|
||||
* @param {string} contentId The id of the content object (unit/component)
|
||||
* @returns {Promise<import("./types.js").ContentData | null>}
|
||||
* @returns {Promise<import("./types.mjs").ContentData | null>}
|
||||
*/
|
||||
export async function getContentData(contentId) {
|
||||
let url;
|
||||
@@ -96,8 +96,8 @@ export async function getContentData(contentId) {
|
||||
/**
|
||||
* Update content object's applied tags
|
||||
* @param {string} contentId The id of the content object (unit/component)
|
||||
* @param {Promise<import("./types.js").UpdateTagsData[]>} tagsData The list of tags (values) to set on content object
|
||||
* @returns {Promise<import("./types.js").ContentTaxonomyTagsData>}
|
||||
* @param {Promise<import("./types.mjs").UpdateTagsData[]>} tagsData The list of tags (values) to set on content object
|
||||
* @returns {Promise<import("./types.mjs").ContentTaxonomyTagsData>}
|
||||
*/
|
||||
export async function updateContentTaxonomyTags(contentId, tagsData) {
|
||||
const url = getContentTaxonomyTagsApiUrl(contentId);
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
|
||||
import { getLibraryId } from '../../generic/key-utils';
|
||||
|
||||
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
|
||||
/** @typedef {import("../../taxonomy/data/types.js").TagData} TagData */
|
||||
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagListData} TagListData */
|
||||
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagData} TagData */
|
||||
|
||||
/**
|
||||
* Builds the query to get the taxonomy tags
|
||||
@@ -126,7 +126,6 @@ export const useContentData = (contentId) => (
|
||||
*/
|
||||
export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
const queryClient = useQueryClient();
|
||||
const unitIframe = window.frames['xblock-iframe'];
|
||||
|
||||
return useMutation({
|
||||
/**
|
||||
@@ -134,7 +133,7 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
* any,
|
||||
* any,
|
||||
* {
|
||||
* tagsData: Promise<import("./types.js").UpdateTagsData[]>
|
||||
* tagsData: Promise<import("./types.mjs").UpdateTagsData[]>
|
||||
* }
|
||||
* >}
|
||||
*/
|
||||
@@ -161,8 +160,7 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
onSuccess: /* istanbul ignore next */ () => {
|
||||
/* istanbul ignore next */
|
||||
if (window.top != null) {
|
||||
// Sends messages to the parent page if the drawer was opened
|
||||
// from an iframe or the unit iframe within the course.
|
||||
// This send messages to the parent page if the drawer is called from a iframe.
|
||||
// Is used on Studio to update tags data and counts.
|
||||
// In the future, when the Course Outline Page and Unit Page are integrated into this MFE,
|
||||
// they should just use React Query to load the tag counts, and React Query will automatically
|
||||
@@ -171,32 +169,26 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
|
||||
// Sends content tags.
|
||||
getContentTaxonomyTagsData(contentId).then((data) => {
|
||||
const contentData = { contentId, ...data };
|
||||
|
||||
const message = {
|
||||
type: 'authoring.events.tags.updated',
|
||||
data: contentData,
|
||||
const contentData = {
|
||||
contentId,
|
||||
...data,
|
||||
};
|
||||
|
||||
const targetOrigin = getConfig().STUDIO_BASE_URL;
|
||||
|
||||
unitIframe?.postMessage(message, targetOrigin);
|
||||
window.top?.postMessage(message, targetOrigin);
|
||||
window.top?.postMessage(
|
||||
{ type: 'authoring.events.tags.updated', data: contentData },
|
||||
getConfig().STUDIO_BASE_URL,
|
||||
);
|
||||
});
|
||||
|
||||
// Sends tags count.
|
||||
getContentTaxonomyTagsCount(contentId).then((count) => {
|
||||
const contentData = { contentId, count };
|
||||
|
||||
const message = {
|
||||
type: 'authoring.events.tags.count.updated',
|
||||
data: contentData,
|
||||
getContentTaxonomyTagsCount(contentId).then((data) => {
|
||||
const contentData = {
|
||||
contentId,
|
||||
count: data,
|
||||
};
|
||||
|
||||
const targetOrigin = getConfig().STUDIO_BASE_URL;
|
||||
|
||||
unitIframe?.postMessage(message, targetOrigin);
|
||||
window.top?.postMessage(message, targetOrigin);
|
||||
window.top?.postMessage(
|
||||
{ type: 'authoring.events.tags.count.updated', data: contentData },
|
||||
getConfig().STUDIO_BASE_URL,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
101
src/content-tags-drawer/data/types.mjs
Normal file
101
src/content-tags-drawer/data/types.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {Object} Tag A tag that has been applied to some content.
|
||||
* @property {string} value The value of the tag, also its ID. e.g. "Biology"
|
||||
* @property {string[]} lineage The values of the tag and its parent(s) in the hierarchy
|
||||
* @property {boolean} canChangeObjecttag
|
||||
* @property {boolean} canDeleteObjecttag
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ContentTaxonomyTagData A list of the tags from one taxonomy that are applied to a content object.
|
||||
* @property {string} name
|
||||
* @property {number} taxonomyId
|
||||
* @property {boolean} canTagObject
|
||||
* @property {Tag[]} tags
|
||||
* @property {string} exportId
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ContentTaxonomyTagsData A list of all the tags applied to some content object, grouped by taxonomy.
|
||||
* @property {ContentTaxonomyTagData[]} taxonomies
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ContentActions
|
||||
* @property {boolean} deleteable
|
||||
* @property {boolean} draggable
|
||||
* @property {boolean} childAddable
|
||||
* @property {boolean} duplicable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} XBlockData
|
||||
* @property {string} id
|
||||
* @property {string} displayName
|
||||
* @property {string} category
|
||||
* @property {boolean} hasChildren
|
||||
* @property {string} editedOn
|
||||
* @property {boolean} published
|
||||
* @property {string} publishedOn
|
||||
* @property {string} studioUrl
|
||||
* @property {boolean} releasedToStudents
|
||||
* @property {string|null} releaseDate
|
||||
* @property {string} visibilityState
|
||||
* @property {boolean} hasExplicitStaffLock
|
||||
* @property {string} start
|
||||
* @property {boolean} graded
|
||||
* @property {string} dueDate
|
||||
* @property {string} due
|
||||
* @property {string|null} relativeWeeksDue
|
||||
* @property {string|null} format
|
||||
* @property {boolean} hasChanges
|
||||
* @property {ContentActions} actions
|
||||
* @property {string} explanatoryMessage
|
||||
* @property {string} showCorrectness
|
||||
* @property {boolean} discussionEnabled
|
||||
* @property {boolean} ancestorHasStaffLock
|
||||
* @property {boolean} staffOnlyMessage
|
||||
* @property {boolean} hasPartitionGroupComponents
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TagsInTaxonomy
|
||||
* @property {boolean} allOrgs
|
||||
* @property {boolean} allowFreeText
|
||||
* @property {boolean} allowMultiple
|
||||
* @property {boolean} canChangeTaxonomy
|
||||
* @property {boolean} canDeleteTaxonomy
|
||||
* @property {boolean} canTagObject
|
||||
* @property {Tag[]} contentTags
|
||||
* @property {string} description
|
||||
* @property {boolean} enabled
|
||||
* @property {string} exportId
|
||||
* @property {number} id
|
||||
* @property {string} name
|
||||
* @property {boolean} systemDefined
|
||||
* @property {number} tagsCount
|
||||
* @property {boolean} visibleToAuthors
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CourseData
|
||||
* @property {string} courseDisplayNameWithDefault
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {XBlockData | CourseData} ContentData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} UpdateTagsData
|
||||
* @property {number} taxonomy
|
||||
* @property {string[]} tags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} StagedTagData
|
||||
* @property {string} value
|
||||
* @property {string} label
|
||||
*/
|
||||
@@ -1,81 +0,0 @@
|
||||
import type { TaxonomyData } from '../../taxonomy/data/types';
|
||||
|
||||
/** A tag that has been applied to some content. */
|
||||
export interface Tag {
|
||||
/** The value of the tag, also its ID. e.g. "Biology" */
|
||||
value: string;
|
||||
/** The values of the tag and its parent(s) in the hierarchy */
|
||||
lineage: string[];
|
||||
canChangeObjecttag: boolean;
|
||||
canDeleteObjecttag: boolean;
|
||||
}
|
||||
|
||||
/** A list of the tags from one taxonomy that are applied to a content object. */
|
||||
export interface ContentTaxonomyTagData {
|
||||
name: string;
|
||||
taxonomyId: number;
|
||||
canTagObject: boolean;
|
||||
tags: Tag[];
|
||||
exportId: string;
|
||||
}
|
||||
|
||||
/** A list of all the tags applied to some content object, grouped by taxonomy. */
|
||||
export interface ContentTaxonomyTagsData {
|
||||
taxonomies: ContentTaxonomyTagData[];
|
||||
}
|
||||
|
||||
export interface ContentActions {
|
||||
deleteable: boolean;
|
||||
draggable: boolean;
|
||||
childAddable: boolean;
|
||||
duplicable: boolean;
|
||||
}
|
||||
|
||||
export interface XBlockData {
|
||||
id: string;
|
||||
displayName: string;
|
||||
category: string;
|
||||
hasChildren: boolean;
|
||||
editedOn: string;
|
||||
published: boolean;
|
||||
publishedOn: string;
|
||||
studioUrl: string;
|
||||
releasedToStudents: boolean;
|
||||
releaseDate: string | null;
|
||||
visibilityState: string;
|
||||
hasExplicitStaffLock: boolean;
|
||||
start: string;
|
||||
graded: boolean;
|
||||
dueDate: string;
|
||||
due: string;
|
||||
relativeWeeksDue: string | null;
|
||||
format: string | null;
|
||||
hasChanges: boolean;
|
||||
actions: ContentActions;
|
||||
explanatoryMessage: string;
|
||||
showCorrectness: string;
|
||||
discussionEnabled: boolean;
|
||||
ancestorHasStaffLock: boolean;
|
||||
staffOnlyMessage: boolean;
|
||||
hasPartitionGroupComponents: boolean;
|
||||
}
|
||||
|
||||
export interface TagsInTaxonomy extends TaxonomyData {
|
||||
contentTags: Tag[];
|
||||
}
|
||||
|
||||
export interface CourseData {
|
||||
courseDisplayNameWithDefault: string;
|
||||
}
|
||||
|
||||
export type ContentData = XBlockData | CourseData;
|
||||
|
||||
export interface UpdateTagsData {
|
||||
taxonomy: number;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface StagedTagData {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as ContentTagsDrawer } from './ContentTagsDrawer';
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as ContentTagsDrawerSheet } from './ContentTagsDrawerSheet';
|
||||
3
src/content-tags-drawer/utils.js
Normal file
3
src/content-tags-drawer/utils.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const extractOrgFromContentId = (contentId) => contentId.split('+')[0].split(':')[1];
|
||||
export const languageExportId = 'languages-v1';
|
||||
@@ -1,2 +0,0 @@
|
||||
export const extractOrgFromContentId = (contentId: string): string => contentId.split('+')[0].split(':')[1];
|
||||
export const languageExportId = 'languages-v1';
|
||||
@@ -1,95 +1,70 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Button, Icon } from '@openedx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Button,
|
||||
Hyperlink,
|
||||
Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { CheckCircle, RadioButtonUnchecked } from '@openedx/paragon/icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { getWaffleFlags } from '../../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const getUpdateLinks = (courseId, waffleFlags) => {
|
||||
const baseUrl = getConfig().STUDIO_BASE_URL;
|
||||
const isLegacyGradingUrl = !waffleFlags.useNewGradingPage;
|
||||
const isLegacyCertificateUrl = !waffleFlags.useNewCertificatesPage;
|
||||
const isLegacyCourseDatesUrl = !waffleFlags.useNewScheduleDetailsPage;
|
||||
const isLegacyOutlineUrl = !waffleFlags.useNewCourseOutlinePage;
|
||||
|
||||
return {
|
||||
welcomeMessage: `/course/${courseId}/course_info`,
|
||||
gradingPolicy: isLegacyGradingUrl
|
||||
? `${baseUrl}/settings/grading/${courseId}` : `/course/${courseId}/settings/grading`,
|
||||
certificate: isLegacyCertificateUrl
|
||||
? `${baseUrl}/certificates/${courseId}` : `/course/${courseId}/certificates`,
|
||||
courseDates: isLegacyCourseDatesUrl
|
||||
? `${baseUrl}/settings/details/${courseId}#schedule` : `/course/${courseId}/settings/details/#schedule`,
|
||||
proctoringEmail: `${baseUrl}/pages-and-resources/proctoring/settings`,
|
||||
outline: isLegacyOutlineUrl ? `${baseUrl}/course/${courseId}` : `/course/${courseId}`,
|
||||
};
|
||||
};
|
||||
|
||||
const ChecklistItemBody = ({
|
||||
courseId,
|
||||
checkId,
|
||||
isCompleted,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const waffleFlags = useSelector(getWaffleFlags);
|
||||
const updateLinks = getUpdateLinks(courseId, waffleFlags);
|
||||
|
||||
return (
|
||||
<ActionRow>
|
||||
<div className="mr-3" id={`icon-${checkId}`} data-testid={`icon-${checkId}`}>
|
||||
{isCompleted ? (
|
||||
<Icon
|
||||
data-testid="completed-icon"
|
||||
src={CheckCircle}
|
||||
className="text-success"
|
||||
style={{ height: '32px', width: '32px' }}
|
||||
screenReaderText={intl.formatMessage(messages.completedItemLabel)}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
data-testid="uncompleted-icon"
|
||||
src={RadioButtonUnchecked}
|
||||
style={{ height: '32px', width: '32px' }}
|
||||
screenReaderText={intl.formatMessage(messages.uncompletedItemLabel)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<FormattedMessage {...messages[`${checkId}ShortDescription`]} />
|
||||
</div>
|
||||
<div className="small">
|
||||
<FormattedMessage {...messages[`${checkId}LongDescription`]} />
|
||||
</div>
|
||||
</div>
|
||||
<ActionRow.Spacer />
|
||||
{updateLinks?.[checkId] && (
|
||||
<Link
|
||||
to={updateLinks[checkId]}
|
||||
data-testid="update-link"
|
||||
>
|
||||
<Button size="sm">
|
||||
<FormattedMessage {...messages.updateLinkLabel} />
|
||||
</Button>
|
||||
</Link>
|
||||
updateLink,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
<ActionRow>
|
||||
<div className="mr-3" id={`icon-${checkId}`} data-testid={`icon-${checkId}`}>
|
||||
{isCompleted ? (
|
||||
<Icon
|
||||
data-testid="completed-icon"
|
||||
src={CheckCircle}
|
||||
className="text-success"
|
||||
style={{ height: '32px', width: '32px' }}
|
||||
screenReaderText={intl.formatMessage(messages.completedItemLabel)}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
data-testid="uncompleted-icon"
|
||||
src={RadioButtonUnchecked}
|
||||
style={{ height: '32px', width: '32px' }}
|
||||
screenReaderText={intl.formatMessage(messages.uncompletedItemLabel)}
|
||||
/>
|
||||
)}
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<FormattedMessage {...messages[`${checkId}ShortDescription`]} />
|
||||
</div>
|
||||
<div className="small">
|
||||
<FormattedMessage {...messages[`${checkId}LongDescription`]} />
|
||||
</div>
|
||||
</div>
|
||||
<ActionRow.Spacer />
|
||||
{updateLink && (
|
||||
<Hyperlink destination={updateLink} data-testid="update-hyperlink">
|
||||
<Button size="sm">
|
||||
<FormattedMessage {...messages.updateLinkLabel} />
|
||||
</Button>
|
||||
</Hyperlink>
|
||||
)}
|
||||
</ActionRow>
|
||||
);
|
||||
|
||||
ChecklistItemBody.defaultProps = {
|
||||
updateLink: null,
|
||||
};
|
||||
|
||||
ChecklistItemBody.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
checkId: PropTypes.string.isRequired,
|
||||
isCompleted: PropTypes.bool.isRequired,
|
||||
updateLink: PropTypes.string,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default ChecklistItemBody;
|
||||
export default injectIntl(ChecklistItemBody);
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { injectIntl, FormattedMessage, FormattedNumber } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { ModeComment } from '@openedx/paragon/icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getWaffleFlags } from '../../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const ChecklistItemComment = ({
|
||||
courseId,
|
||||
checkId,
|
||||
outlineUrl,
|
||||
data,
|
||||
}) => {
|
||||
const waffleFlags = useSelector(getWaffleFlags);
|
||||
|
||||
const getPathToCourseOutlinePage = (assignmentId) => (waffleFlags.useNewCourseOutlinePage
|
||||
? `/course/${courseId}#${assignmentId}` : `${getConfig().STUDIO_BASE_URL}/course/${courseId}#${assignmentId}`);
|
||||
|
||||
const commentWrapper = (comment) => (
|
||||
<div className="row m-0 mt-3 pt-3 border-top align-items-center" data-identifier="comment">
|
||||
<div className="mr-4">
|
||||
@@ -87,9 +79,9 @@ const ChecklistItemComment = ({
|
||||
<ul className="assignment-list">
|
||||
{gradedAssignmentsOutsideDateRange.map(assignment => (
|
||||
<li className="assignment-list-item" key={assignment.id}>
|
||||
<Link to={getPathToCourseOutlinePage(assignment.id)}>
|
||||
<Hyperlink destination={`${outlineUrl}#${assignment.id}`}>
|
||||
{assignment.displayName}
|
||||
</Link>
|
||||
</Hyperlink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -104,7 +96,6 @@ const ChecklistItemComment = ({
|
||||
};
|
||||
|
||||
ChecklistItemComment.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
checkId: PropTypes.string.isRequired,
|
||||
outlineUrl: PropTypes.string.isRequired,
|
||||
data: PropTypes.oneOfType([
|
||||
|
||||
@@ -10,11 +10,11 @@ import ChecklistItemComment from './ChecklistItemComment';
|
||||
import { checklistItems } from './utils/courseChecklistData';
|
||||
|
||||
const ChecklistSection = ({
|
||||
courseId,
|
||||
dataHeading,
|
||||
data,
|
||||
idPrefix,
|
||||
isLoading,
|
||||
updateLinks,
|
||||
}) => {
|
||||
const dataList = checklistItems[idPrefix];
|
||||
const getCompletionCountID = () => (`${idPrefix}-completion-count`);
|
||||
@@ -37,6 +37,8 @@ const ChecklistSection = ({
|
||||
{checks.map(check => {
|
||||
const checkId = check.id;
|
||||
const isCompleted = values[checkId];
|
||||
const updateLink = updateLinks?.[checkId];
|
||||
const outlineUrl = updateLinks.outline;
|
||||
return (
|
||||
<div
|
||||
className={`bg-white border py-3 px-4 ${isCompleted && 'checklist-item-complete'}`}
|
||||
@@ -44,9 +46,9 @@ const ChecklistSection = ({
|
||||
data-testid={`checklist-item-${checkId}`}
|
||||
key={checkId}
|
||||
>
|
||||
<ChecklistItemBody courseId={courseId} {...{ checkId, isCompleted }} />
|
||||
<ChecklistItemBody {...{ checkId, isCompleted, updateLink }} />
|
||||
<div data-testid={`comment-section-${checkId}`}>
|
||||
<ChecklistItemComment {...{ courseId, checkId, data }} />
|
||||
<ChecklistItemComment {...{ checkId, outlineUrl, data }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -59,11 +61,11 @@ const ChecklistSection = ({
|
||||
};
|
||||
|
||||
ChecklistSection.defaultProps = {
|
||||
updateLinks: {},
|
||||
data: {},
|
||||
};
|
||||
|
||||
ChecklistSection.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
dataHeading: PropTypes.string.isRequired,
|
||||
data: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
@@ -127,6 +129,14 @@ ChecklistSection.propTypes = {
|
||||
]),
|
||||
idPrefix: PropTypes.string.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
updateLinks: PropTypes.shape({
|
||||
welcomeMessage: PropTypes.string,
|
||||
gradingPolicy: PropTypes.string,
|
||||
certificate: PropTypes.string,
|
||||
courseDates: PropTypes.string,
|
||||
proctoringEmail: PropTypes.string,
|
||||
outline: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default injectIntl(ChecklistSection);
|
||||
|
||||
@@ -1,49 +1,59 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
|
||||
/* eslint-disable */
|
||||
import {
|
||||
initializeMocks, render, screen, within,
|
||||
} from '../../testUtils';
|
||||
import { getApiWaffleFlagsUrl } from '../../data/api';
|
||||
import { fetchWaffleFlags } from '../../data/thunks';
|
||||
import { generateCourseLaunchData } from '../factories/mockApiResponses';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { checklistItems } from './utils/courseChecklistData';
|
||||
import messages from './messages';
|
||||
render,
|
||||
within,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ChecklistSection from '.';
|
||||
import initializeStore from '../../store';
|
||||
import { initialState,generateCourseLaunchData } from '../factories/mockApiResponses';
|
||||
import messages from './messages';
|
||||
import ChecklistSection from './index';
|
||||
import { checklistItems } from './utils/courseChecklistData';
|
||||
import getUpdateLinks from '../utils';
|
||||
|
||||
const testData = camelCaseObject(generateCourseLaunchData());
|
||||
|
||||
const courseId = '123';
|
||||
|
||||
const defaultProps = {
|
||||
courseId,
|
||||
data: testData,
|
||||
dataHeading: 'Test checklist',
|
||||
idPrefix: 'launchChecklist',
|
||||
updateLinks: getUpdateLinks('courseId'),
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const testChecklistData = checklistItems[defaultProps.idPrefix];
|
||||
|
||||
const completedItemIds = ['welcomeMessage', 'courseDates'];
|
||||
const completedItemIds = ['welcomeMessage', 'courseDates']
|
||||
|
||||
const renderComponent = (props) => {
|
||||
render(<ChecklistSection {...props} />);
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<ChecklistSection {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
let store;
|
||||
|
||||
describe('ChecklistSection', () => {
|
||||
beforeEach(async () => {
|
||||
const { axiosMock, reduxStore } = initializeMocks();
|
||||
axiosMock
|
||||
.onGet(getApiWaffleFlagsUrl(courseId))
|
||||
.reply(200, {
|
||||
useNewGradingPage: true,
|
||||
useNewCertificatesPage: true,
|
||||
useNewScheduleDetailsPage: true,
|
||||
useNewCourseOutlinePage: true,
|
||||
});
|
||||
await executeThunk(fetchWaffleFlags(courseId), reduxStore.dispatch);
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
});
|
||||
|
||||
it('a heading using the dataHeading prop', () => {
|
||||
@@ -54,7 +64,6 @@ describe('ChecklistSection', () => {
|
||||
|
||||
it('completion count text', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const completionText = `${completedItemIds.length}/6 completed`;
|
||||
expect(screen.getByTestId('completion-subheader').textContent).toEqual(completionText);
|
||||
});
|
||||
@@ -113,7 +122,7 @@ describe('ChecklistSection', () => {
|
||||
grades: {
|
||||
...defaultProps.data.grades,
|
||||
sumOfWeights: 1,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
renderComponent(props);
|
||||
@@ -145,7 +154,7 @@ describe('ChecklistSection', () => {
|
||||
...defaultProps.data.assignments,
|
||||
assignmentsWithDatesAfterEnd: [],
|
||||
assignmentsWithOraDatesBeforeStart: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
renderComponent(props);
|
||||
@@ -174,52 +183,73 @@ describe('ChecklistSection', () => {
|
||||
expect(assigmentLinks[1].textContent).toEqual('ORA subsection');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checklist Component', () => {
|
||||
let checklistData;
|
||||
let updateLinks;
|
||||
|
||||
testChecklistData.forEach((check) => {
|
||||
describe(`check with id '${check.id}'`, () => {
|
||||
let checkItem;
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
renderComponent(defaultProps);
|
||||
|
||||
checklistData = testChecklistData.map((item) => ({
|
||||
itemId: item.id,
|
||||
checklistItem: screen.getAllByTestId(`checklist-item-${item.id}`),
|
||||
icon: screen.getAllByTestId(`icon-${item.id}`),
|
||||
shortDescription: messages[`${item.id}ShortDescription`].defaultMessage,
|
||||
longDescription: messages[`${item.id}LongDescription`].defaultMessage,
|
||||
}));
|
||||
|
||||
updateLinks = screen.getAllByTestId('update-link');
|
||||
checkItem = screen.getAllByTestId(`checklist-item-${check.id}`);
|
||||
});
|
||||
|
||||
it('should display the correct icons based on completion status', () => {
|
||||
checklistData.forEach(({ itemId, icon }) => {
|
||||
const { queryByTestId } = within(icon[0]);
|
||||
|
||||
if (completedItemIds.includes(itemId)) {
|
||||
expect(queryByTestId('completed-icon')).not.toBeNull();
|
||||
} else {
|
||||
expect(queryByTestId('uncompleted-icon')).not.toBeNull();
|
||||
}
|
||||
});
|
||||
it('renders', () => {
|
||||
expect(checkItem).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should display short and long descriptions for each checklist item', () => {
|
||||
checklistData.forEach(({ checklistItem, shortDescription, longDescription }) => {
|
||||
const { getByText } = within(checklistItem[0]);
|
||||
it('has correct icon', () => {
|
||||
const icon = screen.getAllByTestId(`icon-${check.id}`)
|
||||
|
||||
expect(getByText(shortDescription)).toBeVisible();
|
||||
expect(getByText(longDescription)).toBeVisible();
|
||||
});
|
||||
expect(icon).toHaveLength(1);
|
||||
|
||||
const { queryByTestId } = within(icon[0]);
|
||||
if (completedItemIds.includes(check.id)) {
|
||||
expect(queryByTestId('completed-icon')).not.toBeNull();
|
||||
} else {
|
||||
expect(queryByTestId('uncompleted-icon')).not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid update links for each checklist item', () => {
|
||||
checklistData.forEach(({ itemId }) => {
|
||||
updateLinks.forEach((link) => {
|
||||
expect(link).toHaveAttribute('href', updateLinks[itemId]);
|
||||
it('has correct short description', () => {
|
||||
const { getByText } = within(checkItem[0]);
|
||||
const shortDescription = messages[`${check.id}ShortDescription`].defaultMessage;
|
||||
expect(getByText(shortDescription)).toBeVisible();
|
||||
});
|
||||
|
||||
it('has correct long description', () => {
|
||||
const { getByText } = within(checkItem[0]);
|
||||
const longDescription = messages[`${check.id}LongDescription`].defaultMessage;
|
||||
expect(getByText(longDescription)).toBeVisible();
|
||||
});
|
||||
|
||||
describe('has correct link', () => {
|
||||
const links = getUpdateLinks('courseId')
|
||||
const shouldShowLink = Object.keys(links).includes(check.id);
|
||||
|
||||
if (shouldShowLink) {
|
||||
it('with a Hyperlink', () => {
|
||||
const { getByRole, getByText } = within(checkItem[0]);
|
||||
|
||||
expect(getByText('Update')).toBeVisible();
|
||||
|
||||
expect(getByRole('link').href).toMatch(links[check.id]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it('without a Hyperlink', () => {
|
||||
const { queryByText } = within(checkItem[0]);
|
||||
|
||||
expect(queryByText('Update')).toBeNull();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import AriaLiveRegion from './AriaLiveRegion';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import ChecklistSection from './ChecklistSection';
|
||||
import { fetchCourseLaunchQuery, fetchCourseBestPracticesQuery } from './data/thunks';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import getUpdateLinks from './utils';
|
||||
|
||||
const CourseChecklist = ({
|
||||
courseId,
|
||||
@@ -23,6 +23,7 @@ const CourseChecklist = ({
|
||||
const dispatch = useDispatch();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const enableQuality = getConfig().ENABLE_CHECKLIST_QUALITY === 'true';
|
||||
const updateLinks = getUpdateLinks(courseId);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseLaunchQuery({ courseId }));
|
||||
@@ -35,19 +36,10 @@ const CourseChecklist = ({
|
||||
bestPracticeData,
|
||||
} = useSelector(state => state.courseChecklist);
|
||||
|
||||
const { bestPracticeChecklistLoadingStatus, launchChecklistLoadingStatus, launchChecklistStatus } = loadingStatus;
|
||||
const { bestPracticeChecklistLoadingStatus, launchChecklistLoadingStatus } = loadingStatus;
|
||||
|
||||
const isCourseLaunchChecklistLoading = bestPracticeChecklistLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isCourseBestPracticeChecklistLoading = launchChecklistLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isLoadingDenied = launchChecklistStatus === RequestStatus.DENIED;
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -74,19 +66,19 @@ const CourseChecklist = ({
|
||||
/>
|
||||
<Stack gap={4}>
|
||||
<ChecklistSection
|
||||
courseId={courseId}
|
||||
dataHeading={intl.formatMessage(messages.launchChecklistLabel)}
|
||||
data={launchData}
|
||||
idPrefix="launchChecklist"
|
||||
isLoading={isCourseLaunchChecklistLoading}
|
||||
updateLinks={updateLinks}
|
||||
/>
|
||||
{enableQuality && (
|
||||
<ChecklistSection
|
||||
courseId={courseId}
|
||||
dataHeading={intl.formatMessage(messages.bestPracticesChecklistLabel)}
|
||||
data={bestPracticeData}
|
||||
idPrefix="bestPracticesChecklist"
|
||||
isLoading={isCourseBestPracticeChecklistLoading}
|
||||
updateLinks={updateLinks}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -149,20 +149,5 @@ describe('CourseChecklistPage', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
const courseLaunchApiUrl = getCourseLaunchApiUrl({
|
||||
courseId, gradedOnly: true, validateOras: true, all: true,
|
||||
});
|
||||
axiosMock.onGet(courseLaunchApiUrl).reply(403);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
const { launchChecklistStatus } = store.getState().courseChecklist.loadingStatus;
|
||||
expect(launchChecklistStatus).toEqual(RequestStatus.DENIED);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,7 @@ export function fetchCourseLaunchQuery({
|
||||
dispatch(fetchLaunchChecklistSuccess({ data }));
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
12
src/course-checklist/utils.js
Normal file
12
src/course-checklist/utils.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const getUpdateLinks = (courseId) => ({
|
||||
welcomeMessage: `${getConfig().STUDIO_BASE_URL}/course_info/${courseId}`,
|
||||
gradingPolicy: `${getConfig().STUDIO_BASE_URL}/settings/grading/${courseId}`,
|
||||
certificate: `${getConfig().STUDIO_BASE_URL}/certificates/${courseId}`,
|
||||
courseDates: `${getConfig().STUDIO_BASE_URL}/settings/details/${courseId}#schedule`,
|
||||
proctoringEmail: 'pages-and-resources/proctoring/settings',
|
||||
outline: `${getConfig().STUDIO_BASE_URL}/course/${courseId}`,
|
||||
});
|
||||
|
||||
export default getUpdateLinks;
|
||||
@@ -1,148 +0,0 @@
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
initializeMocks,
|
||||
render,
|
||||
screen,
|
||||
within,
|
||||
} from '../testUtils';
|
||||
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
|
||||
import mockInfoResult from './__mocks__/courseBlocksInfo.json';
|
||||
import CourseLibraries from './CourseLibraries';
|
||||
import { mockGetEntityLinksByDownstreamContext } from './data/api.mocks';
|
||||
|
||||
mockContentSearchConfig.applyMock();
|
||||
mockGetEntityLinksByDownstreamContext.applyMock();
|
||||
|
||||
const searchEndpoint = 'http://mock.meilisearch.local/indexes/studio/search';
|
||||
|
||||
jest.mock('../studio-home/hooks', () => ({
|
||||
useStudioHome: () => ({
|
||||
isLoadingPage: false,
|
||||
isFailedLoadingPage: false,
|
||||
librariesV2Enabled: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('<CourseLibraries />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
fetchMock.mockReset();
|
||||
|
||||
// The Meilisearch client-side API uses fetch, not Axios.
|
||||
fetchMock.post(searchEndpoint, (_url, req) => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const filter = requestData?.filter[1];
|
||||
const mockInfoResultCopy = cloneDeep(mockInfoResult);
|
||||
const resp = mockInfoResultCopy.filter((o: { filter: string }) => o.filter === filter)[0] || {
|
||||
result: {
|
||||
hits: [],
|
||||
query: '',
|
||||
processingTimeMs: 0,
|
||||
limit: 4,
|
||||
offset: 0,
|
||||
estimatedTotalHits: 0,
|
||||
},
|
||||
};
|
||||
const { result } = resp;
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
const renderCourseLibrariesPage = async (courseKey?: string) => {
|
||||
const courseId = courseKey || mockGetEntityLinksByDownstreamContext.courseKey;
|
||||
render(<CourseLibraries courseId={courseId} />);
|
||||
};
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
// This mock will never return data (it loads forever):
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyLoading);
|
||||
const spinner = await screen.findByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
|
||||
it('shows empty state wheen no links are present', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyEmpty);
|
||||
const emptyMsg = await screen.findByText('This course does not use any content from libraries.');
|
||||
expect(emptyMsg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows alert when out of sync components are present', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(await within(alert).findByText(
|
||||
'1 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
const reviewBtn = await screen.findByRole('button', { name: 'Review' });
|
||||
userEvent.click(reviewBtn);
|
||||
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'false');
|
||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(alert).not.toBeInTheDocument();
|
||||
|
||||
// go back to all tab
|
||||
userEvent.click(allTab);
|
||||
// alert should not be back
|
||||
expect(alert).not.toBeInTheDocument();
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// review updates button
|
||||
const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' });
|
||||
userEvent.click(reviewActionBtn);
|
||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('hide alert on dismiss', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(await within(alert).findByText(
|
||||
'1 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
|
||||
userEvent.click(dismissBtn);
|
||||
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
expect(alert).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows links split by library', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
|
||||
const msg = await screen.findByText('This course contains content from these libraries.');
|
||||
expect(msg).toBeInTheDocument();
|
||||
const allButtons = await screen.findAllByRole('button');
|
||||
// total 3 components used from lib 1
|
||||
const expectedLib1Blocks = 3;
|
||||
// total 4 components used from lib 1
|
||||
const expectedLib2Blocks = 4;
|
||||
// 1 component has updates.
|
||||
const expectedLib2ToUpdate = 1;
|
||||
|
||||
const libraryCards = allButtons.filter((el) => el.classList.contains('collapsible-trigger'));
|
||||
expect(libraryCards.length).toEqual(2);
|
||||
expect(await within(libraryCards[0]).findByText('CS problems 2')).toBeInTheDocument();
|
||||
expect(await within(libraryCards[0]).findByText(`${expectedLib1Blocks} components applied`)).toBeInTheDocument();
|
||||
expect(await within(libraryCards[0]).findByText('All components up to date')).toBeInTheDocument();
|
||||
|
||||
const libParent1 = libraryCards[0].parentElement;
|
||||
expect(libParent1).not.toBeNull();
|
||||
userEvent.click(libraryCards[0]);
|
||||
const xblockCards1 = libParent1!.querySelectorAll('div.card');
|
||||
expect(xblockCards1.length).toEqual(expectedLib1Blocks);
|
||||
|
||||
expect(await within(libraryCards[1]).findByText('CS problems 3')).toBeInTheDocument();
|
||||
expect(await within(libraryCards[1]).findByText(`${expectedLib2Blocks} components applied`)).toBeInTheDocument();
|
||||
expect(await within(libraryCards[1]).findByText(`${expectedLib2ToUpdate} component out of sync`)).toBeInTheDocument();
|
||||
|
||||
const libParent2 = libraryCards[1].parentElement;
|
||||
expect(libParent2).not.toBeNull();
|
||||
userEvent.click(libraryCards[1]);
|
||||
const xblockCards2 = libParent2!.querySelectorAll('div.card');
|
||||
expect(xblockCards2.length).toEqual(expectedLib2Blocks);
|
||||
});
|
||||
});
|
||||
@@ -1,342 +0,0 @@
|
||||
import React, {
|
||||
useCallback, useMemo, useState,
|
||||
} from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Breadcrumb, Button, Card, Collapsible, Container, Dropdown, Hyperlink, Icon, IconButton, Layout, Stack, Tab, Tabs,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Cached, CheckCircle, KeyboardArrowDown, KeyboardArrowRight, Loop, MoreVert,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import {
|
||||
countBy, groupBy, keyBy, tail, uniq,
|
||||
} from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import messages from './messages';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { useEntityLinksByDownstreamContext } from './data/apiHooks';
|
||||
import type { PublishableEntityLink } from './data/api';
|
||||
import { useFetchIndexDocuments } from '../search-manager/data/apiHooks';
|
||||
import { getItemIcon } from '../generic/block-type-utils';
|
||||
import { BlockTypeLabel } from '../search-manager';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import type { ContentHit } from '../search-manager/data/api';
|
||||
import { SearchSortOption } from '../search-manager/data/api';
|
||||
import Loading from '../generic/Loading';
|
||||
import { useStudioHome } from '../studio-home/hooks';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
interface LibraryCardProps {
|
||||
courseId: string;
|
||||
title: string;
|
||||
links: PublishableEntityLink[];
|
||||
}
|
||||
|
||||
interface ComponentInfo extends ContentHit {
|
||||
readyToSync: boolean;
|
||||
}
|
||||
|
||||
interface BlockCardProps {
|
||||
info: ComponentInfo;
|
||||
}
|
||||
|
||||
export enum CourseLibraryTabs {
|
||||
home = '',
|
||||
review = 'review',
|
||||
}
|
||||
|
||||
const BlockCard: React.FC<BlockCardProps> = ({ info }) => {
|
||||
const intl = useIntl();
|
||||
const componentIcon = getItemIcon(info.blockType);
|
||||
const breadcrumbs = tail(info.breadcrumbs) as Array<{ displayName: string, usageKey: string }>;
|
||||
|
||||
const getBlockLink = useCallback(() => {
|
||||
let key = info.usageKey;
|
||||
if (breadcrumbs?.length > 1) {
|
||||
key = breadcrumbs[breadcrumbs.length - 1].usageKey || key;
|
||||
}
|
||||
return `${getConfig().STUDIO_BASE_URL}/container/${key}`;
|
||||
}, [info]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={classNames(
|
||||
'my-3 shadow-none border-light-600 border',
|
||||
{ 'bg-primary-100': info.readyToSync },
|
||||
)}
|
||||
orientation="horizontal"
|
||||
key={info.usageKey}
|
||||
>
|
||||
<Card.Section
|
||||
className="py-2"
|
||||
>
|
||||
<Stack direction="vertical" gap={1}>
|
||||
<Stack direction="horizontal" gap={1} className="micro text-gray-500">
|
||||
<Icon src={componentIcon} size="xs" />
|
||||
<BlockTypeLabel blockType={info.blockType} />
|
||||
<Hyperlink className="lead ml-auto text-black" destination={getBlockLink()} target="_blank">
|
||||
{' '}
|
||||
</Hyperlink>
|
||||
</Stack>
|
||||
<Stack direction="horizontal" className="small" gap={1}>
|
||||
{info.readyToSync && <Icon src={Loop} size="xs" />}
|
||||
{info.formatted?.displayName}
|
||||
</Stack>
|
||||
<div className="micro">{info.formatted?.description}</div>
|
||||
<Breadcrumb
|
||||
className="micro text-gray-500"
|
||||
ariaLabel={intl.formatMessage(messages.breadcrumbAriaLabel)}
|
||||
links={breadcrumbs.map((breadcrumb) => ({ label: breadcrumb.displayName }))}
|
||||
spacer={<span className="custom-spacer">/</span>}
|
||||
linkAs="span"
|
||||
/>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const LibraryCard: React.FC<LibraryCardProps> = ({ courseId, title, links }) => {
|
||||
const intl = useIntl();
|
||||
const linksInfo = useMemo(() => keyBy(links, 'downstreamUsageKey'), [links]);
|
||||
const totalComponents = links.length;
|
||||
const outOfSyncCount = useMemo(() => countBy(links, 'readyToSync').true, [links]);
|
||||
const downstreamKeys = useMemo(() => uniq(Object.keys(linksInfo)), [links]);
|
||||
const { data: downstreamInfo } = useFetchIndexDocuments({
|
||||
filter: [`context_key = "${courseId}"`, `usage_key IN ["${downstreamKeys.join('","')}"]`],
|
||||
limit: downstreamKeys.length,
|
||||
attributesToRetrieve: ['usage_key', 'display_name', 'breadcrumbs', 'description', 'block_type'],
|
||||
attributesToCrop: ['description:30'],
|
||||
sort: [SearchSortOption.TITLE_AZ],
|
||||
}) as unknown as { data: ComponentInfo[] };
|
||||
|
||||
const renderBlockCards = (info: ComponentInfo) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
info.readyToSync = linksInfo[info.usageKey].readyToSync;
|
||||
return <BlockCard info={info} key={info.usageKey} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced>
|
||||
<Collapsible.Trigger className="bg-white shadow px-2 py-2 my-3 collapsible-trigger d-flex font-weight-normal text-dark">
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={KeyboardArrowRight} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={KeyboardArrowDown} />
|
||||
</Collapsible.Visible>
|
||||
<Stack direction="vertical" className="flex-grow-1 pl-2 x-small" gap={1}>
|
||||
<h4>{title}</h4>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<span>
|
||||
{intl.formatMessage(messages.totalComponentLabel, { totalComponents })}
|
||||
</span>
|
||||
<span>/</span>
|
||||
{outOfSyncCount ? (
|
||||
<>
|
||||
<Icon src={Loop} size="xs" />
|
||||
<span>
|
||||
{intl.formatMessage(messages.outOfSyncCountLabel, { outOfSyncCount })}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon src={CheckCircle} size="xs" />
|
||||
<span>
|
||||
{intl.formatMessage(messages.allUptodateLabel)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Dropdown onClick={(e: { stopPropagation: () => void; }) => e.stopPropagation()}>
|
||||
<Dropdown.Toggle
|
||||
id={`dropdown-toggle-${title}`}
|
||||
alt="dropdown-toggle-menu-items"
|
||||
as={IconButton}
|
||||
src={MoreVert}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
disabled
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item>TODO 1</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</Collapsible.Trigger>
|
||||
|
||||
<Collapsible.Body className="collapsible-body border-left border-left-purple px-2">
|
||||
{downstreamInfo?.map(info => renderBlockCards(info))}
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
interface ReviewAlertProps {
|
||||
show: boolean;
|
||||
outOfSyncCount: number;
|
||||
onDismiss: () => void;
|
||||
onReview: () => void;
|
||||
}
|
||||
|
||||
const ReviewAlert: React.FC<ReviewAlertProps> = ({
|
||||
show, outOfSyncCount, onDismiss, onReview,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<AlertMessage
|
||||
title={intl.formatMessage(messages.outOfSyncCountAlertTitle, { outOfSyncCount })}
|
||||
dismissible
|
||||
show={show}
|
||||
icon={Loop}
|
||||
variant="info"
|
||||
onClose={onDismiss}
|
||||
actions={[
|
||||
<Button
|
||||
onClick={onReview}
|
||||
>
|
||||
{intl.formatMessage(messages.outOfSyncCountAlertReviewBtn)}
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TabContent = ({ children }: { children: React.ReactNode }) => (
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
sm={[{ span: 12 }, { span: 12 }]}
|
||||
xs={[{ span: 12 }, { span: 12 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
{children}
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
Help panel
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const [tabKey, setTabKey] = useState<CourseLibraryTabs>(CourseLibraryTabs.home);
|
||||
const [showReviewAlert, setShowReviewAlert] = useState(true);
|
||||
const { data: links, isLoading } = useEntityLinksByDownstreamContext(courseId);
|
||||
const linksByLib = useMemo(() => groupBy(links, 'upstreamContextKey'), [links]);
|
||||
const outOfSyncCount = useMemo(() => countBy(links, 'readyToSync').true, [links]);
|
||||
const {
|
||||
isLoadingPage: isLoadingStudioHome,
|
||||
isFailedLoadingPage: isFailedLoadingStudioHome,
|
||||
librariesV2Enabled,
|
||||
} = useStudioHome();
|
||||
|
||||
const onAlertReview = () => {
|
||||
setTabKey(CourseLibraryTabs.review);
|
||||
setShowReviewAlert(false);
|
||||
};
|
||||
const onAlertDismiss = () => {
|
||||
setShowReviewAlert(false);
|
||||
};
|
||||
|
||||
const renderLibrariesTabContent = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (links?.length === 0) {
|
||||
return <small><FormattedMessage {...messages.homeTabDescriptionEmpty} /></small>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<small><FormattedMessage {...messages.homeTabDescription} /></small>
|
||||
{Object.entries(linksByLib).map(([libKey, libLinks]) => (
|
||||
<LibraryCard
|
||||
courseId={courseId}
|
||||
title={libLinks[0].upstreamContextTitle}
|
||||
links={libLinks}
|
||||
key={libKey}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [links, isLoading, linksByLib]);
|
||||
|
||||
if (!isLoadingStudioHome && (!librariesV2Enabled || isFailedLoadingStudioHome)) {
|
||||
return (
|
||||
<Alert variant="danger">
|
||||
{intl.formatMessage(messages.librariesV2DisabledError)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle))}
|
||||
</title>
|
||||
</Helmet>
|
||||
<Container size="xl" className="px-4 pt-4 mt-3">
|
||||
<ReviewAlert
|
||||
show={outOfSyncCount > 0 && tabKey === CourseLibraryTabs.home && showReviewAlert}
|
||||
outOfSyncCount={outOfSyncCount}
|
||||
onDismiss={onAlertDismiss}
|
||||
onReview={onAlertReview}
|
||||
/>
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
headerActions={!showReviewAlert && tabKey === CourseLibraryTabs.home && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAlertReview}
|
||||
iconBefore={Cached}
|
||||
>
|
||||
{intl.formatMessage(messages.reviewUpdatesBtn)}
|
||||
</Button>
|
||||
)}
|
||||
hideBorder
|
||||
/>
|
||||
<section className="mb-4">
|
||||
<Tabs
|
||||
id="course-library-tabs"
|
||||
activeKey={tabKey}
|
||||
onSelect={(k: CourseLibraryTabs) => setTabKey(k)}
|
||||
>
|
||||
<Tab
|
||||
eventKey={CourseLibraryTabs.home}
|
||||
title={intl.formatMessage(messages.homeTabTitle)}
|
||||
className="px-2 mt-3"
|
||||
>
|
||||
<TabContent>
|
||||
{renderLibrariesTabContent()}
|
||||
</TabContent>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={CourseLibraryTabs.review}
|
||||
title={intl.formatMessage(
|
||||
outOfSyncCount > 0 ? messages.reviewTabTitle : messages.reviewTabTitleEmpty,
|
||||
{ count: outOfSyncCount },
|
||||
)}
|
||||
>
|
||||
<TabContent>Help</TabContent>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</section>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseLibraries;
|
||||
@@ -1,374 +0,0 @@
|
||||
[
|
||||
{
|
||||
"filter": "usage_key IN [\"block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@5be06d70f09c45a581d973d215e7ad62\",\"block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@d1b006742e0745b0948c8ba8dcd9bc07\",\"block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@276914b632eb49049b2c2568ef24fe4d\",\"block-v1:NewOrg3+TSTCS+2025_T1+type@problem+block@fcf430ac5b7a44b6ab316b53a7e67fef\"]",
|
||||
"result": {
|
||||
"hits": [
|
||||
{
|
||||
"display_name": "Dropdown",
|
||||
"description": "asfd sdaf afd",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@problem+block@fcf430ac5b7a44b6ab316b53a7e67fef",
|
||||
"block_type": "problem",
|
||||
"_formatted": {
|
||||
"display_name": "Dropdown",
|
||||
"description": "asfd sdaf afd",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@problem+block@fcf430ac5b7a44b6ab316b53a7e67fef",
|
||||
"block_type": "problem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "HTML 12",
|
||||
"description": "A step beyond the simplicity of the WYSIWYG editor is the Raw HTML component which allows you to use HTML, CSS, and Javascript to create highly flexible content that looks good, excites your learners, and keeps them engaged and coming back to your courses. Unlike many learning platforms, the Open edX platform does not restrict authors in terms of what can be used in HTML components. If you can build it, the platform will not stop you. But use judgement and consider consulting with a web design expert. When used correctly, custom HTML, CSS and Javascript can be powerful tools for a course developer. But they can also lead to experiences that are inaccessible, unpleasant or even insecure to the learner and the provider. 1",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@5be06d70f09c45a581d973d215e7ad62",
|
||||
"block_type": "html",
|
||||
"_formatted": {
|
||||
"display_name": "HTML 12",
|
||||
"description": "A step beyond the simplicity of the WYSIWYG editor is the Raw HTML component which allows you to use HTML, CSS, and Javascript to create highly flexible content that looks…",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@5be06d70f09c45a581d973d215e7ad62",
|
||||
"block_type": "html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Text",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@d1b006742e0745b0948c8ba8dcd9bc07",
|
||||
"block_type": "html",
|
||||
"_formatted": {
|
||||
"display_name": "Text",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@d1b006742e0745b0948c8ba8dcd9bc07",
|
||||
"block_type": "html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Text",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@276914b632eb49049b2c2568ef24fe4d",
|
||||
"block_type": "html",
|
||||
"_formatted": {
|
||||
"display_name": "Text",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@eb4f4db1032f46fdad5a7a9ebde12698"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@eaea16cbcf2d45ba98ddfe72631b9734"
|
||||
},
|
||||
{
|
||||
"display_name": "Problem Bank",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@itembank+block@abca641f6a19447c83e64730808d36d3"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@276914b632eb49049b2c2568ef24fe4d",
|
||||
"block_type": "html"
|
||||
}
|
||||
}
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": 0,
|
||||
"limit": 4,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"filter": "usage_key IN [\"block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@0dfbabb8c93140caa3a004ab621757f5\",\"block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@37a916b53c2f430989d33ddeb47e224d\",\"block-v1:NewOrg3+TSTCS+2025_T1+type@video+block@33518787c3e14844ad823af8ff158a83\"]",
|
||||
"result": {
|
||||
"hits": [
|
||||
{
|
||||
"display_name": "Edited title",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@d6e696f3d7d14cde82876a72a75cb208"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@05da683dc74e405ca355c6b90d58ad6e"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@video+block@33518787c3e14844ad823af8ff158a83",
|
||||
"block_type": "video",
|
||||
"_formatted": {
|
||||
"display_name": "Edited title",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@d6e696f3d7d14cde82876a72a75cb208"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@05da683dc74e405ca355c6b90d58ad6e"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@video+block@33518787c3e14844ad823af8ff158a83",
|
||||
"block_type": "video"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Text 1",
|
||||
"description": " 8¹⁺² 3² Accept change now!d",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@d6e696f3d7d14cde82876a72a75cb208"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@bab96d80267041138975e91a96e20f35"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@0dfbabb8c93140caa3a004ab621757f5",
|
||||
"block_type": "html",
|
||||
"_formatted": {
|
||||
"display_name": "Text 1",
|
||||
"description": " 8¹⁺² 3² Accept change now!d",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@d6e696f3d7d14cde82876a72a75cb208"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@bab96d80267041138975e91a96e20f35"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@0dfbabb8c93140caa3a004ab621757f5",
|
||||
"block_type": "html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Text 23",
|
||||
"description": " AB = \\begin{pmatrix} 7 & 10 \\\\ 13 & 18 \\end{pmatrix} ",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@d6e696f3d7d14cde82876a72a75cb208"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@bab96d80267041138975e91a96e20f35"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@37a916b53c2f430989d33ddeb47e224d",
|
||||
"block_type": "html",
|
||||
"_formatted": {
|
||||
"display_name": "Text 23",
|
||||
"description": " AB = \\begin{pmatrix} 7 & 10 \\\\ 13 & 18 \\end{pmatrix} ",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Learn UNIXY"
|
||||
},
|
||||
{
|
||||
"display_name": "Section",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@chapter+block@6104ecc39b0046c5888db9e64db30887"
|
||||
},
|
||||
{
|
||||
"display_name": "Subsection",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@sequential+block@d6e696f3d7d14cde82876a72a75cb208"
|
||||
},
|
||||
{
|
||||
"display_name": "Unit",
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@vertical+block@bab96d80267041138975e91a96e20f35"
|
||||
}
|
||||
],
|
||||
"usage_key": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@37a916b53c2f430989d33ddeb47e224d",
|
||||
"block_type": "html"
|
||||
}
|
||||
}
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": 0,
|
||||
"limit": 3,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,100 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 970,
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamVersion": 15,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB2:html:c0c1ca28-ff25-4757-83bc-3a2c2a0fe9c8",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@0dfbabb8c93140caa3a004ab621757f5",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 15,
|
||||
"versionDeclined": 13,
|
||||
"created": "2025-02-08T14:11:23.650589Z",
|
||||
"updated": "2025-02-08T14:11:23.650589Z"
|
||||
},
|
||||
{
|
||||
"id": 971,
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamVersion": 3,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB2:html:fd2d3827-e633-4217-bca9-c6661086b4b2",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@37a916b53c2f430989d33ddeb47e224d",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 3,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:11:23.650589Z",
|
||||
"updated": "2025-02-08T14:11:23.650589Z"
|
||||
},
|
||||
{
|
||||
"id": 972,
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamVersion": 3,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB2:video:ba2023d4-b4e4-44a5-bfc8-322203e8737f",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@video+block@33518787c3e14844ad823af8ff158a83",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 3,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:11:23.650589Z",
|
||||
"updated": "2025-02-08T14:11:23.650589Z"
|
||||
},
|
||||
{
|
||||
"id": 974,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 18,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@5be06d70f09c45a581d973d215e7ad62",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 17,
|
||||
"versionDeclined": 18,
|
||||
"created": "2025-02-12T05:38:53.967738Z",
|
||||
"updated": "2025-02-12T05:41:01.225542Z"
|
||||
},
|
||||
{
|
||||
"id": 975,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 1,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:4abdfa10-dd1a-4ebb-bad3-489000671acb",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@d1b006742e0745b0948c8ba8dcd9bc07",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 1,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-12T05:38:55.899821Z",
|
||||
"updated": "2025-02-12T05:38:55.899821Z"
|
||||
},
|
||||
{
|
||||
"id": 976,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 1,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:6aff1b41-e406-41ff-9d31-70d02ef42deb",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@276914b632eb49049b2c2568ef24fe4d",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 1,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-12T05:38:57.228152Z",
|
||||
"updated": "2025-02-12T05:38:57.228152Z"
|
||||
},
|
||||
{
|
||||
"id": 977,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 3,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@problem+block@fcf430ac5b7a44b6ab316b53a7e67fef",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-12T05:38:58.538280Z",
|
||||
"updated": "2025-02-12T05:38:58.538280Z"
|
||||
}
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
import mockLinksResult from '../__mocks__/publishableEntityLinks.json';
|
||||
import { createAxiosError } from '../../testUtils';
|
||||
import * as api from './api';
|
||||
|
||||
/**
|
||||
* Mock for `getEntityLinksByDownstreamContext()`
|
||||
*
|
||||
* This mock returns a fixed response for the downstreamContextKey.
|
||||
*/
|
||||
export async function mockGetEntityLinksByDownstreamContext(
|
||||
downstreamContextKey: string,
|
||||
): Promise<api.PublishableEntityLink[]> {
|
||||
switch (downstreamContextKey) {
|
||||
case mockGetEntityLinksByDownstreamContext.invalidCourseKey:
|
||||
throw createAxiosError({
|
||||
code: 404,
|
||||
message: 'Not found.',
|
||||
path: api.getEntityLinksByDownstreamContextUrl(downstreamContextKey),
|
||||
});
|
||||
case mockGetEntityLinksByDownstreamContext.courseKeyLoading:
|
||||
return new Promise(() => {});
|
||||
case mockGetEntityLinksByDownstreamContext.courseKeyEmpty:
|
||||
return Promise.resolve([]);
|
||||
default:
|
||||
return Promise.resolve(mockGetEntityLinksByDownstreamContext.response);
|
||||
}
|
||||
}
|
||||
mockGetEntityLinksByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||
mockGetEntityLinksByDownstreamContext.invalidCourseKey = 'course_key_error';
|
||||
mockGetEntityLinksByDownstreamContext.courseKeyLoading = 'courseKeyLoading';
|
||||
mockGetEntityLinksByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
|
||||
mockGetEntityLinksByDownstreamContext.response = mockLinksResult;
|
||||
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
|
||||
mockGetEntityLinksByDownstreamContext.applyMock = () => {
|
||||
jest.spyOn(api, 'getEntityLinksByDownstreamContext').mockImplementation(mockGetEntityLinksByDownstreamContext);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
|
||||
export const getEntityLinksByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/upstreams/${downstreamContextKey}`;
|
||||
|
||||
export interface PublishableEntityLink {
|
||||
upstreamUsageKey: string;
|
||||
upstreamContextKey: string;
|
||||
upstreamContextTitle: string;
|
||||
upstreamVersion: string;
|
||||
downstreamUsageKey: string;
|
||||
downstreamContextTitle: string;
|
||||
downstreamContextKey: string;
|
||||
versionSynced: string;
|
||||
versionDeclined: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
readyToSync: boolean;
|
||||
}
|
||||
|
||||
export const getEntityLinksByDownstreamContext = async (
|
||||
downstreamContextKey: string,
|
||||
): Promise<PublishableEntityLink[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksByDownstreamContextUrl(downstreamContextKey));
|
||||
return camelCaseObject(data);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { getEntityLinksByDownstreamContextUrl } from './api';
|
||||
import { useEntityLinksByDownstreamContext } from './apiHooks';
|
||||
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('course libraries api hooks', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('should create library block', async () => {
|
||||
const courseKey = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl(courseKey);
|
||||
axiosMock.onGet(url).reply(200, []);
|
||||
const { result } = renderHook(() => useEntityLinksByDownstreamContext(courseKey), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
expect(result.current.data).toEqual([]);
|
||||
expect(axiosMock.history.get[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import {
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { getEntityLinksByDownstreamContext } from './api';
|
||||
|
||||
export const courseLibrariesQueryKeys = {
|
||||
all: ['courseLibraries'],
|
||||
courseLibraries: (courseKey?: string) => [...courseLibrariesQueryKeys.all, courseKey],
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch a content library by its ID.
|
||||
*/
|
||||
export const useEntityLinksByDownstreamContext = (courseKey: string | undefined) => (
|
||||
useQuery({
|
||||
queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey),
|
||||
queryFn: () => getEntityLinksByDownstreamContext(courseKey!),
|
||||
enabled: courseKey !== undefined,
|
||||
})
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './CourseLibraries';
|
||||
@@ -1,81 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
headingTitle: {
|
||||
id: 'course-authoring.course-libraries.header.title',
|
||||
defaultMessage: 'Libraries',
|
||||
description: 'Title for page',
|
||||
},
|
||||
headingSubtitle: {
|
||||
id: 'course-authoring.course-libraries.header.subtitle',
|
||||
defaultMessage: 'Content',
|
||||
description: 'Subtitle for page',
|
||||
},
|
||||
homeTabTitle: {
|
||||
id: 'course-authoring.course-libraries.tab.home.title',
|
||||
defaultMessage: 'Libraries',
|
||||
description: 'Tab title for home tab',
|
||||
},
|
||||
homeTabDescription: {
|
||||
id: 'course-authoring.course-libraries.tab.home.description',
|
||||
defaultMessage: 'This course contains content from these libraries.',
|
||||
description: 'Description text for home tab',
|
||||
},
|
||||
homeTabDescriptionEmpty: {
|
||||
id: 'course-authoring.course-libraries.tab.home.description-no-links',
|
||||
defaultMessage: 'This course does not use any content from libraries.',
|
||||
description: 'Description text for home tab',
|
||||
},
|
||||
reviewTabTitle: {
|
||||
id: 'course-authoring.course-libraries.tab.review.title',
|
||||
defaultMessage: 'Review Content Updates ({count})',
|
||||
description: 'Tab title for review tab',
|
||||
},
|
||||
reviewTabTitleEmpty: {
|
||||
id: 'course-authoring.course-libraries.tab.review.title-no-updates',
|
||||
defaultMessage: 'Review Content Updates',
|
||||
description: 'Tab title for review tab when no updates are available',
|
||||
},
|
||||
breadcrumbAriaLabel: {
|
||||
id: 'course-authoring.course-libraries.downstream-block.breadcrumb.aria-label',
|
||||
defaultMessage: 'Component breadcrumb',
|
||||
description: 'Aria label for breadcrumb in component cards in course libraries page.',
|
||||
},
|
||||
totalComponentLabel: {
|
||||
id: 'course-authoring.course-libraries.libcard.total-component.label',
|
||||
defaultMessage: '{totalComponents, plural, one {# component} other {# components}} applied',
|
||||
description: 'Prints total components applied from library',
|
||||
},
|
||||
allUptodateLabel: {
|
||||
id: 'course-authoring.course-libraries.libcard.up-to-date.label',
|
||||
defaultMessage: 'All components up to date',
|
||||
description: 'Shown if all components under a library are up to date',
|
||||
},
|
||||
outOfSyncCountLabel: {
|
||||
id: 'course-authoring.course-libraries.libcard.out-of-sync.label',
|
||||
defaultMessage: '{outOfSyncCount, plural, one {# component} other {# components}} out of sync',
|
||||
description: 'Prints number of components out of sync from library',
|
||||
},
|
||||
outOfSyncCountAlertTitle: {
|
||||
id: 'course-authoring.course-libraries.libcard.out-of-sync.alert.title',
|
||||
defaultMessage: '{outOfSyncCount} library components are out of sync. Review updates to accept or ignore changes',
|
||||
description: 'Alert message shown when library components are out of sync',
|
||||
},
|
||||
reviewUpdatesBtn: {
|
||||
id: 'course-authoring.course-libraries.libcard.review-updates.btn.text',
|
||||
defaultMessage: 'Review Updates',
|
||||
description: 'Action button to review updates',
|
||||
},
|
||||
outOfSyncCountAlertReviewBtn: {
|
||||
id: 'course-authoring.course-libraries.libcard.out-of-sync.alert.review-btn-text',
|
||||
defaultMessage: 'Review',
|
||||
description: 'Alert review button text',
|
||||
},
|
||||
librariesV2DisabledError: {
|
||||
id: 'course-authoring.course-libraries.alert.error.libraries.v2.disabled',
|
||||
defaultMessage: 'This page cannot be shown: Libraries v2 are disabled.',
|
||||
description: 'Error message shown to users when trying to load a libraries V2 page while libraries v2 are disabled.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -68,7 +68,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading,
|
||||
isLoadingDenied,
|
||||
isReIndexShow,
|
||||
showSuccessAlert,
|
||||
isSectionsExpanded,
|
||||
@@ -234,27 +233,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="px-4 mt-4">
|
||||
<PageAlerts
|
||||
courseId={courseId}
|
||||
notificationDismissUrl={notificationDismissUrl}
|
||||
handleDismissNotification={handleDismissNotification}
|
||||
discussionsSettings={discussionsSettings}
|
||||
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
|
||||
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
|
||||
deprecatedBlocksInfo={deprecatedBlocksInfo}
|
||||
proctoringErrors={proctoringErrors}
|
||||
mfeProctoredExamSettingsUrl={mfeProctoredExamSettingsUrl}
|
||||
advanceSettingsUrl={advanceSettingsUrl}
|
||||
savingStatus={savingStatus}
|
||||
errors={errors}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
|
||||
@@ -597,10 +597,10 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check whether section, subsection and unit is deleted when corresponding delete button is clicked', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { findAllByTestId, findByTestId, queryByText } = render(<RootWrapper />);
|
||||
// get section, subsection and unit
|
||||
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
|
||||
const [sectionElement] = await screen.findAllByTestId('section-card');
|
||||
const [sectionElement] = await findAllByTestId('section-card');
|
||||
const [subsection] = section.childInfo.children;
|
||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
||||
@@ -610,7 +610,7 @@ describe('<CourseOutline />', () => {
|
||||
|
||||
const checkDeleteBtn = async (item, element, elementName) => {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(item.displayName), `Failed for ${elementName}!`).toBeInTheDocument();
|
||||
expect(queryByText(item.displayName), `Failed for ${elementName}!`).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock.onDelete(getCourseItemApiUrl(item.id)).reply(200);
|
||||
@@ -619,11 +619,11 @@ describe('<CourseOutline />', () => {
|
||||
fireEvent.click(menu);
|
||||
const deleteButton = await within(element).findByTestId(`${elementName}-card-header__menu-delete-button`);
|
||||
fireEvent.click(deleteButton);
|
||||
const confirmButton = await screen.findByRole('button', { name: 'Delete' });
|
||||
fireEvent.click(confirmButton);
|
||||
const confirmButton = await findByTestId('delete-confirm-button');
|
||||
await act(async () => fireEvent.click(confirmButton));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(item.displayName), `Failed for ${elementName}!`).not.toBeInTheDocument();
|
||||
expect(queryByText(item.displayName), `Failed for ${elementName}!`).not.toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2291,18 +2291,4 @@ describe('<CourseOutline />', () => {
|
||||
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus;
|
||||
expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.item-card-header__title-btn {
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
flex: 1 1 0%;
|
||||
width: fit-content;
|
||||
height: 1.5rem;
|
||||
margin-right: .25rem;
|
||||
background: transparent;
|
||||
@@ -15,7 +15,6 @@
|
||||
.item-card-edit-icon {
|
||||
opacity: 0;
|
||||
transition: opacity .3s linear;
|
||||
margin-right: .5rem;
|
||||
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
|
||||
@@ -87,5 +87,4 @@ export const API_ERROR_TYPES = /** @type {const} */ ({
|
||||
networkError: 'networkError',
|
||||
serverError: 'serverError',
|
||||
unknown: 'unknown',
|
||||
forbidden: 'forbidden',
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ const slice = createSlice({
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
fetchStatusBarSelfPacedSuccess: (state, { payload }) => {
|
||||
fetchStatusBarSelPacedSuccess: (state, { payload }) => {
|
||||
state.statusBarData.isSelfPaced = payload.isSelfPaced;
|
||||
},
|
||||
updateSavingStatus: (state, { payload }) => {
|
||||
@@ -206,7 +206,7 @@ export const {
|
||||
updateStatusBar,
|
||||
updateCourseActions,
|
||||
fetchStatusBarChecklistSuccess,
|
||||
fetchStatusBarSelfPacedSuccess,
|
||||
fetchStatusBarSelPacedSuccess,
|
||||
updateFetchSectionLoadingStatus,
|
||||
updateCourseLaunchQueryStatus,
|
||||
updateSavingStatus,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { updateClipboardData } from '../../generic/data/slice';
|
||||
import { NOTIFICATION_MESSAGES } from '../../constants';
|
||||
import { COURSE_BLOCK_NAMES } from '../constants';
|
||||
import { API_ERROR_TYPES, COURSE_BLOCK_NAMES } from '../constants';
|
||||
import {
|
||||
hideProcessingNotification,
|
||||
showProcessingNotification,
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
getCourseBestPracticesChecklist,
|
||||
getCourseLaunchChecklist,
|
||||
} from '../utils/getChecklistForStatusBar';
|
||||
import { getErrorDetails } from '../utils/getErrorDetails';
|
||||
import {
|
||||
addNewCourseItem,
|
||||
deleteCourseItem,
|
||||
@@ -42,7 +41,7 @@ import {
|
||||
updateStatusBar,
|
||||
updateCourseActions,
|
||||
fetchStatusBarChecklistSuccess,
|
||||
fetchStatusBarSelfPacedSuccess,
|
||||
fetchStatusBarSelPacedSuccess,
|
||||
updateSavingStatus,
|
||||
updateSectionList,
|
||||
updateFetchSectionLoadingStatus,
|
||||
@@ -55,6 +54,24 @@ import {
|
||||
updateCourseLaunchQueryStatus,
|
||||
} from './slice';
|
||||
|
||||
const getErrorDetails = (error, dismissible = true) => {
|
||||
const errorInfo = { dismissible };
|
||||
if (error.response?.data) {
|
||||
const { data } = error.response;
|
||||
if ((typeof data === 'string' && !data.includes('</html>')) || typeof data === 'object') {
|
||||
errorInfo.data = JSON.stringify(data);
|
||||
}
|
||||
errorInfo.status = error.response.status;
|
||||
errorInfo.type = API_ERROR_TYPES.serverError;
|
||||
} else if (error.request) {
|
||||
errorInfo.type = API_ERROR_TYPES.networkError;
|
||||
} else {
|
||||
errorInfo.type = API_ERROR_TYPES.unknown;
|
||||
errorInfo.data = error.message;
|
||||
}
|
||||
return errorInfo;
|
||||
};
|
||||
|
||||
export function fetchCourseOutlineIndexQuery(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
@@ -82,16 +99,10 @@ export function fetchCourseOutlineIndexQuery(courseId) {
|
||||
|
||||
dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateOutlineIndexLoadingStatus({
|
||||
status: RequestStatus.DENIED,
|
||||
}));
|
||||
} else {
|
||||
dispatch(updateOutlineIndexLoadingStatus({
|
||||
status: RequestStatus.FAILED,
|
||||
errors: getErrorDetails(error, false),
|
||||
}));
|
||||
}
|
||||
dispatch(updateOutlineIndexLoadingStatus({
|
||||
status: RequestStatus.FAILED,
|
||||
errors: getErrorDetails(error, false),
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -108,7 +119,7 @@ export function fetchCourseLaunchQuery({
|
||||
const data = await getCourseLaunch({
|
||||
courseId, gradedOnly, validateOras, all,
|
||||
});
|
||||
dispatch(fetchStatusBarSelfPacedSuccess({ isSelfPaced: data.isSelfPaced }));
|
||||
dispatch(fetchStatusBarSelPacedSuccess({ isSelfPaced: data.isSelfPaced }));
|
||||
dispatch(fetchStatusBarChecklistSuccess(getCourseLaunchChecklist(data)));
|
||||
|
||||
dispatch(updateCourseLaunchQueryStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { copyToClipboard } from '../generic/data/thunks';
|
||||
import { getSavingStatus as getGenericSavingStatus } from '../generic/data/selectors';
|
||||
import { getWaffleFlags } from '../data/selectors';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { COURSE_BLOCK_NAMES } from './constants';
|
||||
import {
|
||||
@@ -59,7 +58,6 @@ import {
|
||||
const useCourseOutline = ({ courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const waffleFlags = useSelector(getWaffleFlags);
|
||||
|
||||
const {
|
||||
reindexLink,
|
||||
@@ -114,7 +112,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
};
|
||||
|
||||
const getUnitUrl = (locator) => {
|
||||
if (getConfig().ENABLE_UNIT_PAGE === 'true' && waffleFlags.useNewUnitPage) {
|
||||
if (getConfig().ENABLE_UNIT_PAGE === 'true') {
|
||||
return `/course/${courseId}/container/${locator}`;
|
||||
}
|
||||
return `${getConfig().STUDIO_BASE_URL}/container/${locator}`;
|
||||
@@ -122,7 +120,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
|
||||
const openUnitPage = (locator) => {
|
||||
const url = getUnitUrl(locator);
|
||||
if (getConfig().ENABLE_UNIT_PAGE === 'true' && waffleFlags.useNewUnitPage) {
|
||||
if (getConfig().ENABLE_UNIT_PAGE === 'true') {
|
||||
navigate(url);
|
||||
} else {
|
||||
window.location.assign(url);
|
||||
@@ -242,7 +240,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
};
|
||||
|
||||
const handleDismissNotification = () => {
|
||||
dispatch(dismissNotificationQuery(`${getConfig().STUDIO_BASE_URL}${notificationDismissUrl}`));
|
||||
dispatch(dismissNotificationQuery(notificationDismissUrl));
|
||||
};
|
||||
|
||||
const handleSectionDragAndDrop = (
|
||||
@@ -302,7 +300,6 @@ const useCourseOutline = ({ courseId }) => {
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,
|
||||
isLoadingDenied: outlineIndexLoadingStatus === RequestStatus.DENIED,
|
||||
isReIndexShow: Boolean(reindexLink),
|
||||
showSuccessAlert,
|
||||
isDisabledReindexButton,
|
||||
@@ -361,4 +358,5 @@ const useCourseOutline = ({ courseId }) => {
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useCourseOutline };
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as CourseOutline } from './CourseOutline';
|
||||
|
||||
@@ -65,4 +65,5 @@ const getFormattedSidebarMessages = (docsLinks, intl) => {
|
||||
];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { getFormattedSidebarMessages };
|
||||
|
||||
@@ -343,38 +343,13 @@ const PageAlerts = ({
|
||||
const renderApiErrors = () => {
|
||||
let errorList = Object.entries(errors).filter(obj => obj[1] !== null).map(([k, v]) => {
|
||||
switch (v.type) {
|
||||
case API_ERROR_TYPES.forbidden: {
|
||||
const description = intl.formatMessage(messages.forbiddenAlertBody, {
|
||||
LMS: (
|
||||
<Hyperlink
|
||||
destination={`${getConfig().LMS_BASE_URL}`}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{intl.formatMessage(messages.forbiddenAlertLmsUrl)}
|
||||
</Hyperlink>
|
||||
),
|
||||
});
|
||||
case API_ERROR_TYPES.serverError:
|
||||
return {
|
||||
key: k,
|
||||
desc: description,
|
||||
title: intl.formatMessage(messages.forbiddenAlert),
|
||||
dismissible: v.dismissible,
|
||||
};
|
||||
}
|
||||
case API_ERROR_TYPES.serverError: {
|
||||
const description = (
|
||||
<Truncate lines={2}>
|
||||
{v.data || intl.formatMessage(messages.serverErrorAlertBody)}
|
||||
</Truncate>
|
||||
);
|
||||
return {
|
||||
key: k,
|
||||
desc: description,
|
||||
desc: v.data || intl.formatMessage(messages.serverErrorAlertBody),
|
||||
title: intl.formatMessage(messages.serverErrorAlert),
|
||||
dismissible: v.dismissible,
|
||||
};
|
||||
}
|
||||
case API_ERROR_TYPES.networkError:
|
||||
return {
|
||||
key: k,
|
||||
@@ -403,7 +378,7 @@ const PageAlerts = ({
|
||||
dismissError={() => dispatch(dismissError(msgObj.key))}
|
||||
>
|
||||
<Alert.Heading>{msgObj.title}</Alert.Heading>
|
||||
{msgObj.desc}
|
||||
{msgObj.desc && <Truncate lines={2}>{msgObj.desc}</Truncate>}
|
||||
</ErrorAlert>
|
||||
) : (
|
||||
<Alert
|
||||
@@ -412,7 +387,7 @@ const PageAlerts = ({
|
||||
key={msgObj.key}
|
||||
>
|
||||
<Alert.Heading>{msgObj.title}</Alert.Heading>
|
||||
{msgObj.desc}
|
||||
{msgObj.desc && <Truncate lines={2}>{msgObj.desc}</Truncate>}
|
||||
</Alert>
|
||||
)
|
||||
))
|
||||
|
||||
@@ -71,19 +71,19 @@ describe('<PageAlerts />', () => {
|
||||
});
|
||||
|
||||
it('renders null when no alerts are present', () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByTestId('browser-router')).toBeEmptyDOMElement();
|
||||
const { queryByTestId } = renderComponent();
|
||||
expect(queryByTestId('browser-router')).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders configuration alerts', async () => {
|
||||
renderComponent({
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
notificationDismissUrl: 'some-url',
|
||||
handleDismissNotification,
|
||||
});
|
||||
|
||||
expect(screen.queryByText(messages.configurationErrorTitle.defaultMessage)).toBeInTheDocument();
|
||||
const dismissBtn = screen.queryByText('Dismiss');
|
||||
expect(queryByText(messages.configurationErrorTitle.defaultMessage)).toBeInTheDocument();
|
||||
const dismissBtn = queryByText('Dismiss');
|
||||
await act(async () => fireEvent.click(dismissBtn));
|
||||
|
||||
expect(handleDismissNotification).toBeCalled();
|
||||
@@ -117,7 +117,7 @@ describe('<PageAlerts />', () => {
|
||||
});
|
||||
|
||||
it('renders deprecation warning alerts', async () => {
|
||||
renderComponent({
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
deprecatedBlocksInfo: {
|
||||
blocks: [['url1', 'block1'], ['url2']],
|
||||
@@ -126,20 +126,20 @@ describe('<PageAlerts />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByText(messages.deprecationWarningTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.deprecationWarningBlocksText.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText('block1')).toHaveAttribute('href', 'url1');
|
||||
expect(screen.queryByText(messages.deprecatedComponentName.defaultMessage)).toHaveAttribute('href', 'url2');
|
||||
expect(queryByText(messages.deprecationWarningTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.deprecationWarningBlocksText.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText('block1')).toHaveAttribute('href', 'url1');
|
||||
expect(queryByText(messages.deprecatedComponentName.defaultMessage)).toHaveAttribute('href', 'url2');
|
||||
|
||||
const feedbackLink = screen.queryByText(messages.advancedSettingLinkText.defaultMessage);
|
||||
const feedbackLink = queryByText(messages.advancedSettingLinkText.defaultMessage);
|
||||
expect(feedbackLink).toBeInTheDocument();
|
||||
expect(feedbackLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}/some-url`);
|
||||
expect(screen.queryByText('lti')).toBeInTheDocument();
|
||||
expect(screen.queryByText('video')).toBeInTheDocument();
|
||||
expect(queryByText('lti')).toBeInTheDocument();
|
||||
expect(queryByText('video')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders proctoring alerts with mfe settings link', async () => {
|
||||
renderComponent({
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
mfeProctoredExamSettingsUrl: 'mfe-url',
|
||||
proctoringErrors: [
|
||||
@@ -148,15 +148,15 @@ describe('<PageAlerts />', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(screen.queryByText('error 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('error 2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('message 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('message 2')).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.proctoredSettingsLinkText.defaultMessage)).toHaveAttribute('href', 'mfe-url');
|
||||
expect(queryByText('error 1')).toBeInTheDocument();
|
||||
expect(queryByText('error 2')).toBeInTheDocument();
|
||||
expect(queryByText('message 1')).toBeInTheDocument();
|
||||
expect(queryByText('message 2')).toBeInTheDocument();
|
||||
expect(queryByText(messages.proctoredSettingsLinkText.defaultMessage)).toHaveAttribute('href', 'mfe-url');
|
||||
});
|
||||
|
||||
it('renders proctoring alerts without mfe settings link', async () => {
|
||||
renderComponent({
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
advanceSettingsUrl: '/some-url',
|
||||
proctoringErrors: [
|
||||
@@ -165,11 +165,11 @@ describe('<PageAlerts />', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(screen.queryByText('error 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('error 2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('message 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('message 2')).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.advancedSettingLinkText.defaultMessage)).toHaveAttribute(
|
||||
expect(queryByText('error 1')).toBeInTheDocument();
|
||||
expect(queryByText('error 2')).toBeInTheDocument();
|
||||
expect(queryByText('message 1')).toBeInTheDocument();
|
||||
expect(queryByText('message 2')).toBeInTheDocument();
|
||||
expect(queryByText(messages.advancedSettingLinkText.defaultMessage)).toHaveAttribute(
|
||||
'href',
|
||||
`${getConfig().STUDIO_BASE_URL}/some-url`,
|
||||
);
|
||||
@@ -181,10 +181,10 @@ describe('<PageAlerts />', () => {
|
||||
conflictingFiles: [],
|
||||
errorFiles: ['error.css'],
|
||||
});
|
||||
renderComponent();
|
||||
expect(screen.queryByText(messages.newFileAlertTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.errorFileAlertTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.newFileAlertAction.defaultMessage)).toHaveAttribute(
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText(messages.newFileAlertTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.errorFileAlertTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.newFileAlertAction.defaultMessage)).toHaveAttribute(
|
||||
'href',
|
||||
`${getConfig().STUDIO_BASE_URL}/assets/course-id`,
|
||||
);
|
||||
@@ -196,16 +196,16 @@ describe('<PageAlerts />', () => {
|
||||
conflictingFiles: ['some.css', 'some.js'],
|
||||
errorFiles: [],
|
||||
});
|
||||
renderComponent();
|
||||
expect(screen.queryByText(messages.conflictingFileAlertTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.newFileAlertAction.defaultMessage)).toHaveAttribute(
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText(messages.conflictingFileAlertTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.newFileAlertAction.defaultMessage)).toHaveAttribute(
|
||||
'href',
|
||||
`${getConfig().STUDIO_BASE_URL}/assets/course-id`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders api error alerts', async () => {
|
||||
renderComponent({
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
errors: {
|
||||
outlineIndexApi: { data: 'some error', status: 400, type: API_ERROR_TYPES.serverError },
|
||||
@@ -213,34 +213,9 @@ describe('<PageAlerts />', () => {
|
||||
reindexApi: { type: API_ERROR_TYPES.unknown, data: 'some unknown error' },
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText(messages.networkErrorAlert.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.serverErrorAlert.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText('some error')).toBeInTheDocument();
|
||||
expect(screen.queryByText('some unknown error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders forbidden api error alerts', async () => {
|
||||
renderComponent({
|
||||
...pageAlertsData,
|
||||
errors: {
|
||||
outlineIndexApi: {
|
||||
data: 'some error', status: 403, type: API_ERROR_TYPES.forbidden, dismissable: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText(messages.forbiddenAlert.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.queryByText(messages.forbiddenAlertBody.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders api error alerts when status is not 403', async () => {
|
||||
renderComponent({
|
||||
...pageAlertsData,
|
||||
errors: {
|
||||
outlineIndexApi: {
|
||||
data: 'some error', status: 500, type: API_ERROR_TYPES.serverError, dismissable: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText('some error')).toBeInTheDocument();
|
||||
expect(queryByText(messages.networkErrorAlert.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.serverErrorAlert.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText('some error')).toBeInTheDocument();
|
||||
expect(queryByText('some unknown error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,21 +121,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Network error',
|
||||
description: 'Generic network error alert.',
|
||||
},
|
||||
forbiddenAlert: {
|
||||
id: 'course-authoring.course-outline.page-alert.forbidden.title',
|
||||
defaultMessage: 'Access Restricted',
|
||||
description: 'Forbidden(403) alert title',
|
||||
},
|
||||
forbiddenAlertBody: {
|
||||
id: 'course-authoring.course-outline.page-alert.forbidden.body',
|
||||
defaultMessage: 'It looks like you’re trying to access a page you don’t have permission to view. Contact your admin if you think this is a mistake, or head back to the {LMS}.',
|
||||
description: 'Forbidden(403) alert body',
|
||||
},
|
||||
forbiddenAlertLmsUrl: {
|
||||
id: 'course-authoring.course-outline.page-alert.lms',
|
||||
defaultMessage: 'LMS',
|
||||
description: 'LMS base redirection url',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import moment from 'moment/moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -6,14 +6,11 @@ import { getConfig } from '@edx/frontend-platform/config';
|
||||
import {
|
||||
Button, Hyperlink, Form, Stack, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { ContentTagsDrawerSheet } from '../../content-tags-drawer';
|
||||
import TagCount from '../../generic/tag-count';
|
||||
import { useHelpUrls } from '../../help-urls/hooks';
|
||||
import { getWaffleFlags } from '../../data/selectors';
|
||||
import { VIDEO_SHARING_OPTIONS } from '../constants';
|
||||
import { useContentTagsCount } from '../../generic/data/apiHooks';
|
||||
import messages from './messages';
|
||||
@@ -46,7 +43,6 @@ const StatusBar = ({
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { config } = useContext(AppContext);
|
||||
const waffleFlags = useSelector(getWaffleFlags);
|
||||
|
||||
const {
|
||||
courseReleaseDate,
|
||||
@@ -66,6 +62,7 @@ const StatusBar = ({
|
||||
|
||||
const courseReleaseDateObj = moment.utc(courseReleaseDate, 'MMM DD, YYYY at HH:mm UTC', true);
|
||||
const checkListTitle = `${completedCourseLaunchChecks + completedCourseBestPracticesChecks}/${totalCourseLaunchChecks + totalCourseBestPracticesChecks}`;
|
||||
const checklistDestination = () => new URL(`checklists/${courseId}`, config.STUDIO_BASE_URL).href;
|
||||
const scheduleDestination = () => new URL(`settings/details/${courseId}#schedule`, config.STUDIO_BASE_URL).href;
|
||||
|
||||
const {
|
||||
@@ -85,9 +82,10 @@ const StatusBar = ({
|
||||
<>
|
||||
<Stack direction="horizontal" gap={3.5} className="d-flex align-items-stretch outline-status-bar" data-testid="outline-status-bar">
|
||||
<StatusBarItem title={intl.formatMessage(messages.startDateTitle)}>
|
||||
<Link
|
||||
<Hyperlink
|
||||
className="small"
|
||||
to={waffleFlags.useNewScheduleDetailsPage ? `/course/${courseId}/settings/details/#schedule` : scheduleDestination()}
|
||||
destination={scheduleDestination()}
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{courseReleaseDateObj.isValid() ? (
|
||||
<FormattedDate
|
||||
@@ -99,7 +97,7 @@ const StatusBar = ({
|
||||
minute="numeric"
|
||||
/>
|
||||
) : courseReleaseDate}
|
||||
</Link>
|
||||
</Hyperlink>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem title={intl.formatMessage(messages.pacingTypeTitle)}>
|
||||
<span className="small">
|
||||
@@ -109,12 +107,13 @@ const StatusBar = ({
|
||||
</span>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem title={intl.formatMessage(messages.checklistTitle)}>
|
||||
<Link
|
||||
<Hyperlink
|
||||
className="small"
|
||||
to={`/course/${courseId}/checklists`}
|
||||
destination={checklistDestination()}
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{checkListTitle} {intl.formatMessage(messages.checklistCompleted)}
|
||||
</Link>
|
||||
</Hyperlink>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem title={intl.formatMessage(messages.highlightEmailsTitle)}>
|
||||
<div className="d-flex align-items-center">
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { API_ERROR_TYPES } from '../constants';
|
||||
|
||||
export const getErrorDetails = (error, dismissible = true) => {
|
||||
const errorInfo = { dismissible };
|
||||
if (error.response?.status === 403) {
|
||||
// For 403 status the error shouldn't be dismissible
|
||||
errorInfo.dismissible = false;
|
||||
errorInfo.type = API_ERROR_TYPES.forbidden;
|
||||
errorInfo.status = error.response.status;
|
||||
} else if (error.response?.data) {
|
||||
const { data } = error.response;
|
||||
if ((typeof data === 'string' && !data.includes('</html>')) || typeof data === 'object') {
|
||||
errorInfo.data = JSON.stringify(data);
|
||||
}
|
||||
errorInfo.status = error.response.status;
|
||||
errorInfo.type = API_ERROR_TYPES.serverError;
|
||||
} else if (error.request) {
|
||||
errorInfo.type = API_ERROR_TYPES.networkError;
|
||||
} else {
|
||||
errorInfo.type = API_ERROR_TYPES.unknown;
|
||||
errorInfo.data = error.message;
|
||||
}
|
||||
return errorInfo;
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { getErrorDetails } from './getErrorDetails';
|
||||
import { API_ERROR_TYPES } from '../constants';
|
||||
|
||||
describe('getErrorDetails', () => {
|
||||
it('should handle 403 status error', () => {
|
||||
const error = { response: { data: 'some data', status: 403 } };
|
||||
const result = getErrorDetails(error);
|
||||
expect(result).toEqual({ dismissible: false, status: 403, type: API_ERROR_TYPES.forbidden });
|
||||
});
|
||||
|
||||
it('should handle response with data', () => {
|
||||
const error = { response: { data: 'some data', status: 500 } };
|
||||
const result = getErrorDetails(error);
|
||||
expect(result).toEqual({
|
||||
dismissible: true, data: '"some data"', status: 500, type: API_ERROR_TYPES.serverError,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle response with HTML data', () => {
|
||||
const error = { response: { data: '<html>error</html>', status: 500 } };
|
||||
const result = getErrorDetails(error);
|
||||
expect(result).toEqual({ dismissible: true, status: 500, type: API_ERROR_TYPES.serverError });
|
||||
});
|
||||
|
||||
it('should handle request error', () => {
|
||||
const error = { request: {} };
|
||||
const result = getErrorDetails(error);
|
||||
expect(result).toEqual({ dismissible: true, type: API_ERROR_TYPES.networkError });
|
||||
});
|
||||
|
||||
it('should handle unknown error', () => {
|
||||
const error = { message: 'Unknown error' };
|
||||
const result = getErrorDetails(error);
|
||||
expect(result).toEqual({ dismissible: true, type: API_ERROR_TYPES.unknown, data: 'Unknown error' });
|
||||
});
|
||||
});
|
||||
@@ -56,4 +56,5 @@ const useCourseRerun = (courseId) => {
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useCourseRerun };
|
||||
|
||||
@@ -20,7 +20,6 @@ import CourseTeamMember from './course-team-member/CourseTeamMember';
|
||||
import InfoModal from './info-modal/InfoModal';
|
||||
import { useCourseTeam } from './hooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
|
||||
const CourseTeam = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
@@ -36,7 +35,6 @@ const CourseTeam = ({ courseId }) => {
|
||||
courseTeamUsers,
|
||||
currentUserEmail,
|
||||
isLoading,
|
||||
isLoadingDenied,
|
||||
isSingleAdmin,
|
||||
isFormVisible,
|
||||
isQueryPending,
|
||||
@@ -57,14 +55,6 @@ const CourseTeam = ({ courseId }) => {
|
||||
handleInternetConnectionFailed,
|
||||
} = useCourseTeam({ intl, courseId });
|
||||
|
||||
if (isLoadingDenied) {
|
||||
return (
|
||||
<Container size="xl" className="course-unit px-4 mt-4">
|
||||
<ConnectionErrorAlert />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
@@ -17,7 +18,6 @@ import CourseTeam from './CourseTeam';
|
||||
import messages from './messages';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { executeThunk } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { changeRoleTeamUserQuery, deleteCourseTeamQuery } from './data/thunk';
|
||||
|
||||
let axiosMock;
|
||||
@@ -219,31 +219,4 @@ describe('<CourseTeam />', () => {
|
||||
await executeThunk(changeRoleTeamUserQuery(courseId, 'staff@example.com', { role: USER_ROLES.admin }), store.dispatch);
|
||||
expect(getAllByText('Admin')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
const { loadingCourseTeamStatus } = store.getState().courseTeam;
|
||||
expect(loadingCourseTeamStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets loading status to FAILED upon receiving a 404 response from the API', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(404);
|
||||
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const { loadingCourseTeamStatus } = store.getState().courseTeam;
|
||||
expect(loadingCourseTeamStatus).toEqual(RequestStatus.FAILED);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,7 @@ export function fetchCourseTeamQuery(courseId) {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
dispatch(updateLoadingCourseTeamStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,7 +113,6 @@ const useCourseTeam = ({ courseId }) => {
|
||||
courseTeamUsers,
|
||||
currentUserEmail,
|
||||
isLoading: loadingCourseTeamStatus === RequestStatus.IN_PROGRESS,
|
||||
isLoadingDenied: loadingCourseTeamStatus === RequestStatus.DENIED,
|
||||
isSingleAdmin,
|
||||
isFormVisible,
|
||||
isAllowActions,
|
||||
@@ -136,4 +135,5 @@ const useCourseTeam = ({ courseId }) => {
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useCourseTeam };
|
||||
|
||||
@@ -49,4 +49,5 @@ const getInfoModalSettings = (modalType, currentEmail, errorMessage, courseName,
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { getInfoModalSettings };
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Container, Layout, Stack, Button, TransitionReplace,
|
||||
} from '@openedx/paragon';
|
||||
import { Container, Layout, Stack } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { Warning as WarningIcon } from '@openedx/paragon/icons';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
|
||||
import DraggableList from '../editors/sharedComponents/DraggableList';
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
@@ -23,20 +20,18 @@ import { SavingErrorAlert } from '../generic/saving-error-alert';
|
||||
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
|
||||
import Loading from '../generic/Loading';
|
||||
import AddComponent from './add-component/AddComponent';
|
||||
import CourseXBlock from './course-xblock/CourseXBlock';
|
||||
import HeaderTitle from './header-title/HeaderTitle';
|
||||
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
||||
import HeaderNavigations from './header-navigations/HeaderNavigations';
|
||||
import Sequence from './course-sequence';
|
||||
import Sidebar from './sidebar';
|
||||
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
|
||||
import { useCourseUnit } from './hooks';
|
||||
import messages from './messages';
|
||||
import PublishControls from './sidebar/PublishControls';
|
||||
import LocationInfo from './sidebar/LocationInfo';
|
||||
import TagsSidebarControls from '../content-tags-drawer/tags-sidebar-controls';
|
||||
import { PasteNotificationAlert } from './clipboard';
|
||||
import XBlockContainerIframe from './xblock-container-iframe';
|
||||
import MoveModal from './move-modal';
|
||||
import PreviewLibraryXBlockChanges from './preview-changes';
|
||||
|
||||
const CourseUnit = ({ courseId }) => {
|
||||
const { blockId } = useParams();
|
||||
@@ -45,13 +40,10 @@ const CourseUnit = ({ courseId }) => {
|
||||
isLoading,
|
||||
sequenceId,
|
||||
unitTitle,
|
||||
unitCategory,
|
||||
errorMessage,
|
||||
sequenceStatus,
|
||||
savingStatus,
|
||||
isTitleEditFormOpen,
|
||||
isUnitVerticalType,
|
||||
isUnitLibraryType,
|
||||
staticFileNotices,
|
||||
currentlyVisibleToStudents,
|
||||
unitXBlockActions,
|
||||
@@ -64,22 +56,20 @@ const CourseUnit = ({ courseId }) => {
|
||||
handleCreateNewCourseXBlock,
|
||||
handleConfigureSubmit,
|
||||
courseVerticalChildren,
|
||||
handleXBlockDragAndDrop,
|
||||
canPasteComponent,
|
||||
isMoveModalOpen,
|
||||
openMoveModal,
|
||||
closeMoveModal,
|
||||
movedXBlockParams,
|
||||
handleRollbackMovedXBlock,
|
||||
handleCloseXBlockMovedAlert,
|
||||
handleNavigateToTargetUnit,
|
||||
} = useCourseUnit({ courseId, blockId });
|
||||
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);
|
||||
|
||||
const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]);
|
||||
const [unitXBlocks, setUnitXBlocks] = useState(initialXBlocksData);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getPageHeadTitle('', unitTitle);
|
||||
}, [unitTitle]);
|
||||
|
||||
useScrollToLastPosition();
|
||||
useEffect(() => {
|
||||
setUnitXBlocks(courseVerticalChildren.children);
|
||||
}, [courseVerticalChildren.children]);
|
||||
|
||||
const {
|
||||
isShow: isShowProcessingNotification,
|
||||
@@ -98,44 +88,16 @@ const CourseUnit = ({ courseId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const finalizeXBlockOrder = () => (newXBlocks) => {
|
||||
handleXBlockDragAndDrop(newXBlocks.map(xBlock => xBlock.id), () => {
|
||||
setUnitXBlocks(initialXBlocksData);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container size="xl" className="course-unit px-4">
|
||||
<section className="course-unit-container mb-4 mt-5">
|
||||
<TransitionReplace>
|
||||
{movedXBlockParams.isSuccess ? (
|
||||
<AlertMessage
|
||||
key="xblock-moved-alert"
|
||||
data-testid="xblock-moved-alert"
|
||||
show={movedXBlockParams.isSuccess}
|
||||
variant="success"
|
||||
icon={CheckCircleIcon}
|
||||
title={movedXBlockParams.isUndo
|
||||
? intl.formatMessage(messages.alertMoveCancelTitle)
|
||||
: intl.formatMessage(messages.alertMoveSuccessTitle)}
|
||||
description={movedXBlockParams.isUndo
|
||||
? intl.formatMessage(messages.alertMoveCancelDescription, { title: movedXBlockParams.title })
|
||||
: intl.formatMessage(messages.alertMoveSuccessDescription, { title: movedXBlockParams.title })}
|
||||
aria-hidden={movedXBlockParams.isSuccess}
|
||||
dismissible
|
||||
actions={movedXBlockParams.isUndo ? null : [
|
||||
<Button
|
||||
onClick={handleRollbackMovedXBlock}
|
||||
key="xblock-moved-alert-undo-move-button"
|
||||
>
|
||||
{intl.formatMessage(messages.undoMoveButton)}
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={handleNavigateToTargetUnit}
|
||||
key="xblock-moved-alert-new-location-button"
|
||||
>
|
||||
{intl.formatMessage(messages.newLocationButton)}
|
||||
</Button>,
|
||||
]}
|
||||
onClose={handleCloseXBlockMovedAlert}
|
||||
/>
|
||||
) : null}
|
||||
</TransitionReplace>
|
||||
<SubHeader
|
||||
hideBorder
|
||||
title={(
|
||||
@@ -148,28 +110,28 @@ const CourseUnit = ({ courseId }) => {
|
||||
/>
|
||||
)}
|
||||
breadcrumbs={(
|
||||
<Breadcrumbs
|
||||
courseId={courseId}
|
||||
parentUnitId={sequenceId}
|
||||
/>
|
||||
<Breadcrumbs />
|
||||
)}
|
||||
headerActions={(
|
||||
<HeaderNavigations
|
||||
unitCategory={unitCategory}
|
||||
headerNavigationsActions={headerNavigationsActions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isUnitVerticalType && (
|
||||
<Sequence
|
||||
courseId={courseId}
|
||||
sequenceId={sequenceId}
|
||||
unitId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
showPasteUnit={showPasteUnit}
|
||||
/>
|
||||
)}
|
||||
<Layout {...layoutGrid}>
|
||||
<Sequence
|
||||
courseId={courseId}
|
||||
sequenceId={sequenceId}
|
||||
unitId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
showPasteUnit={showPasteUnit}
|
||||
/>
|
||||
<Layout
|
||||
lg={[{ span: 8 }, { span: 4 }]}
|
||||
md={[{ span: 8 }, { span: 4 }]}
|
||||
sm={[{ span: 8 }, { span: 3 }]}
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
{currentlyVisibleToStudents && (
|
||||
<AlertMessage
|
||||
@@ -185,51 +147,63 @@ const CourseUnit = ({ courseId }) => {
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
<XBlockContainerIframe
|
||||
courseId={courseId}
|
||||
<Stack className="mb-4 course-unit__xblocks">
|
||||
<DraggableList
|
||||
itemList={unitXBlocks}
|
||||
setState={setUnitXBlocks}
|
||||
updateOrder={finalizeXBlockOrder}
|
||||
>
|
||||
<SortableContext
|
||||
id="root"
|
||||
items={unitXBlocks}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{unitXBlocks.map(({
|
||||
name, id, blockType: type, shouldScroll, userPartitionInfo, validationMessages,
|
||||
}) => (
|
||||
<CourseXBlock
|
||||
id={id}
|
||||
key={id}
|
||||
title={name}
|
||||
type={type}
|
||||
blockId={blockId}
|
||||
validationMessages={validationMessages}
|
||||
shouldScroll={shouldScroll}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
data-testid="course-xblock"
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DraggableList>
|
||||
</Stack>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
courseVerticalChildren={courseVerticalChildren.children}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
/>
|
||||
{isUnitVerticalType && (
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
/>
|
||||
)}
|
||||
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
|
||||
{showPasteXBlock && canPasteComponent && (
|
||||
<PasteComponent
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={handleCreateNewCourseXBlock}
|
||||
text={intl.formatMessage(messages.pasteButtonText)}
|
||||
/>
|
||||
)}
|
||||
<MoveModal
|
||||
isOpenModal={isMoveModalOpen}
|
||||
openModal={openMoveModal}
|
||||
closeModal={closeMoveModal}
|
||||
courseId={courseId}
|
||||
/>
|
||||
<PreviewLibraryXBlockChanges />
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<Stack gap={3}>
|
||||
{isUnitVerticalType && (
|
||||
<>
|
||||
<Sidebar data-testid="course-unit-sidebar">
|
||||
<PublishControls blockId={blockId} />
|
||||
</Sidebar>
|
||||
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<Sidebar className="tags-sidebar">
|
||||
<TagsSidebarControls />
|
||||
</Sidebar>
|
||||
)}
|
||||
<Sidebar data-testid="course-unit-location-sidebar">
|
||||
<LocationInfo />
|
||||
</Sidebar>
|
||||
</>
|
||||
<Sidebar data-testid="course-unit-sidebar">
|
||||
<PublishControls blockId={blockId} />
|
||||
</Sidebar>
|
||||
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'
|
||||
&& (
|
||||
<Sidebar className="tags-sidebar">
|
||||
<TagsSidebarControls />
|
||||
</Sidebar>
|
||||
)}
|
||||
<Sidebar data-testid="course-unit-location-sidebar">
|
||||
<LocationInfo />
|
||||
</Sidebar>
|
||||
</Stack>
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
@import "./breadcrumbs/Breadcrumbs";
|
||||
@import "./course-sequence/CourseSequence";
|
||||
@import "./add-component/AddComponent";
|
||||
@import "./course-xblock/CourseXBlock";
|
||||
@import "./sidebar/Sidebar";
|
||||
@import "./header-title/HeaderTitle";
|
||||
@import "./move-modal";
|
||||
@import "./preview-changes";
|
||||
|
||||
.course-unit {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.course-unit__alert {
|
||||
margin-bottom: 1.75rem;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import {
|
||||
act, render, waitFor, within, screen,
|
||||
act, render, waitFor, fireEvent, within, screen,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
getCourseSectionVerticalApiUrl,
|
||||
getCourseUnitApiUrl,
|
||||
getCourseVerticalChildrenApiUrl,
|
||||
getCourseOutlineInfoUrl,
|
||||
getXBlockBaseApiUrl,
|
||||
postXBlockBaseApiUrl,
|
||||
} from './data/api';
|
||||
@@ -29,8 +28,6 @@ import {
|
||||
fetchCourseSectionVerticalData,
|
||||
fetchCourseUnitQuery,
|
||||
fetchCourseVerticalChildrenData,
|
||||
getCourseOutlineInfoQuery,
|
||||
patchUnitItemQuery,
|
||||
} from './data/thunk';
|
||||
import initializeStore from '../store';
|
||||
import {
|
||||
@@ -40,11 +37,13 @@ import {
|
||||
courseUnitMock,
|
||||
courseVerticalChildrenMock,
|
||||
clipboardMockResponse,
|
||||
courseOutlineInfoMock,
|
||||
} from './__mocks__';
|
||||
import { clipboardUnit, clipboardXBlock } from '../__mocks__';
|
||||
import {
|
||||
clipboardUnit,
|
||||
clipboardXBlock,
|
||||
} from '../__mocks__';
|
||||
import { executeThunk } from '../utils';
|
||||
import { IFRAME_FEATURE_POLICY } from '../constants';
|
||||
import deleteModalMessages from '../generic/delete-modal/messages';
|
||||
import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
|
||||
import pasteNotificationsMessages from './clipboard/paste-notification/messages';
|
||||
import headerNavigationsMessages from './header-navigations/messages';
|
||||
@@ -54,16 +53,13 @@ import sidebarMessages from './sidebar/messages';
|
||||
import { extractCourseUnitId } from './sidebar/utils';
|
||||
import CourseUnit from './CourseUnit';
|
||||
|
||||
import tagsDrawerMessages from '../content-tags-drawer/messages';
|
||||
import { getClipboardUrl } from '../generic/data/api';
|
||||
import configureModalMessages from '../generic/configure-modal/messages';
|
||||
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
|
||||
import courseXBlockMessages from './course-xblock/messages';
|
||||
import addComponentMessages from './add-component/messages';
|
||||
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
|
||||
import { IframeProvider } from './context/iFrameContext';
|
||||
import moveModalMessages from './move-modal/messages';
|
||||
import xblockContainerIframeMessages from './xblock-container-iframe/messages';
|
||||
import { PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
|
||||
import messages from './messages';
|
||||
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
@@ -72,13 +68,6 @@ const blockId = '567890';
|
||||
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
const userName = 'openedx';
|
||||
const handleConfigureSubmitMock = jest.fn();
|
||||
|
||||
const {
|
||||
block_id: id,
|
||||
user_partition_info: userPartitionInfo,
|
||||
} = courseVerticalChildrenMock.children[0];
|
||||
const userPartitionInfoFormatted = camelCaseObject(userPartitionInfo);
|
||||
|
||||
const postXBlockBody = {
|
||||
parent_locator: blockId,
|
||||
@@ -93,9 +82,6 @@ jest.mock('react-router-dom', () => ({
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn(({ queryKey }) => {
|
||||
const taxonomyApiHooksModule = jest.requireActual('../taxonomy/data/apiHooks');
|
||||
const actualQueryKeys = taxonomyApiHooksModule.taxonomyQueryKeys;
|
||||
|
||||
if (queryKey[0] === 'contentTaxonomyTags') {
|
||||
return {
|
||||
data: {
|
||||
@@ -109,14 +95,6 @@ jest.mock('@tanstack/react-query', () => ({
|
||||
isSuccess: true,
|
||||
};
|
||||
}
|
||||
if (actualQueryKeys.all.includes(queryKey[0])) {
|
||||
return {
|
||||
data: {
|
||||
results: [],
|
||||
},
|
||||
isSuccess: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: {},
|
||||
isSuccess: true,
|
||||
@@ -125,9 +103,6 @@ jest.mock('@tanstack/react-query', () => ({
|
||||
useQueryClient: jest.fn(() => ({
|
||||
setQueryData: jest.fn(),
|
||||
})),
|
||||
useMutation: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
@@ -137,28 +112,10 @@ const clipboardBroadcastChannelMock = {
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
/**
|
||||
* Simulates receiving a post message event for testing purposes.
|
||||
* This can be used to mimic events like deletion or other actions
|
||||
* sent from Backbone or other sources via postMessage.
|
||||
*
|
||||
* @param {string} type - The type of the message event (e.g., 'deleteXBlock').
|
||||
* @param {Object} payload - The payload data for the message event.
|
||||
*/
|
||||
function simulatePostMessageEvent(type, payload) {
|
||||
const messageEvent = new MessageEvent('message', {
|
||||
data: { type, payload },
|
||||
});
|
||||
|
||||
window.dispatchEvent(messageEvent);
|
||||
}
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<IframeProvider>
|
||||
<CourseUnit courseId={courseId} />
|
||||
</IframeProvider>
|
||||
<CourseUnit courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
@@ -173,13 +130,9 @@ describe('<CourseUnit />', () => {
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
window.scrollTo = jest.fn();
|
||||
global.localStorage.clear();
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getClipboardUrl())
|
||||
.reply(200, clipboardUnit);
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
@@ -217,396 +170,6 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the course unit iframe with correct attributes', async () => {
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
|
||||
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;');
|
||||
expect(iframe).toHaveAttribute('scrolling', 'no');
|
||||
expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
|
||||
expect(iframe).toHaveAttribute('loading', 'lazy');
|
||||
expect(iframe).toHaveAttribute('frameborder', '0');
|
||||
});
|
||||
});
|
||||
|
||||
it('adjusts iframe height dynamically based on courseXBlockDropdownHeight postMessage event', async () => {
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;');
|
||||
simulatePostMessageEvent(messageTypes.toggleCourseXBlockDropdown, {
|
||||
courseXBlockDropdownHeight: 200,
|
||||
});
|
||||
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 200px;');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an error alert when a studioAjaxError message is received', async () => {
|
||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.studioAjaxError, {
|
||||
error: 'Some error text...',
|
||||
});
|
||||
});
|
||||
expect(getByTestId('saving-error-alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = getByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the xBlocks iframe and opens the tags drawer on postMessage event', async () => {
|
||||
const { getByTitle, getByText } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
});
|
||||
|
||||
simulatePostMessageEvent(messageTypes.openManageTags, { contentId: blockId });
|
||||
|
||||
expect(getByText(tagsDrawerMessages.headerSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
|
||||
const { getByTitle, queryByTitle } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
|
||||
const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
|
||||
|
||||
const legacyXBlockEditModalIframe = queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.releaseStatusTitle.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarBodyNote.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).queryByRole('button', {
|
||||
name: sidebarMessages.actionButtonPublishTitle.defaultMessage,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates course unit sidebar after receiving refreshPositions message', async () => {
|
||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.refreshPositions);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.releaseStatusTitle.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarBodyNote.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(courseUnitSidebar).queryByRole('button', {
|
||||
name: sidebarMessages.actionButtonPublishTitle.defaultMessage,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
|
||||
const {
|
||||
getByTitle, getByText, queryByRole, getAllByRole, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length),
|
||||
);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.deleteXBlock, {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
|
||||
expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
// Find the Cancel and Delete buttons within the iframe by their specific classes
|
||||
const cancelButton = getAllByRole('button', { name: /Cancel/i })
|
||||
.find(({ classList }) => classList.contains('btn-tertiary'));
|
||||
const deleteButton = getAllByRole('button', { name: /Delete/i })
|
||||
.find(({ classList }) => classList.contains('btn-primary'));
|
||||
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
|
||||
simulatePostMessageEvent(messageTypes.deleteXBlock, {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
userEvent.click(deleteButton);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.makePublic,
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
|
||||
.reply(200, { dummy: 'value' });
|
||||
await executeThunk(deleteUnitItemQuery(
|
||||
courseId,
|
||||
courseVerticalChildrenMock.children[0].block_id,
|
||||
simulatePostMessageEvent,
|
||||
), store.dispatch);
|
||||
|
||||
const updatedCourseVerticalChildren = courseVerticalChildrenMock.children.filter(
|
||||
child => child.block_id !== courseVerticalChildrenMock.children[0].block_id,
|
||||
);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, {
|
||||
children: updatedCourseVerticalChildren,
|
||||
isPublished: false,
|
||||
canPasteComponent: true,
|
||||
});
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
);
|
||||
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
|
||||
const {
|
||||
getByTitle, getByRole, getByText, queryByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({
|
||||
parent_locator: blockId,
|
||||
duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id,
|
||||
}))
|
||||
.replyOnce(200, { locator: '1234567890' });
|
||||
|
||||
const updatedCourseVerticalChildren = [
|
||||
...courseVerticalChildrenMock.children,
|
||||
{
|
||||
...courseVerticalChildrenMock.children[0],
|
||||
name: 'New Cloned XBlock',
|
||||
},
|
||||
];
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseVerticalChildrenMock,
|
||||
children: updatedCourseVerticalChildren,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length),
|
||||
);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.makePublic,
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
);
|
||||
|
||||
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles CourseUnit header action buttons', async () => {
|
||||
const { open } = window;
|
||||
window.open = jest.fn();
|
||||
@@ -658,19 +221,6 @@ describe('<CourseUnit />', () => {
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
xblock: {
|
||||
...courseSectionVerticalMock.xblock,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
@@ -680,13 +230,12 @@ describe('<CourseUnit />', () => {
|
||||
.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
});
|
||||
expect(titleEditField).not.toBeInTheDocument();
|
||||
userEvent.click(editTitleButton);
|
||||
fireEvent.click(editTitleButton);
|
||||
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
|
||||
await userEvent.clear(titleEditField);
|
||||
await userEvent.type(titleEditField, newDisplayName);
|
||||
await userEvent.tab();
|
||||
|
||||
fireEvent.change(titleEditField, { target: { value: newDisplayName } });
|
||||
await act(async () => {
|
||||
fireEvent.blur(titleEditField);
|
||||
});
|
||||
expect(titleEditField).toHaveValue(newDisplayName);
|
||||
|
||||
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
@@ -740,7 +289,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const problemButton = getByRole('button', {
|
||||
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(problemButton);
|
||||
@@ -775,44 +324,6 @@ describe('<CourseUnit />', () => {
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handle creating Text xblock and saves scroll position in localStorage', async () => {
|
||||
const { getByText, getByRole } = render(<RootWrapper />);
|
||||
const xblockType = 'text';
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
|
||||
window.scrollTo(0, 250);
|
||||
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
|
||||
|
||||
await waitFor(() => {
|
||||
const textButton = screen.getByRole('button', { name: /Text/i });
|
||||
|
||||
expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
userEvent.click(textButton);
|
||||
|
||||
const addXBlockDialog = getByRole('dialog');
|
||||
expect(addXBlockDialog).toBeInTheDocument();
|
||||
|
||||
expect(getByText(
|
||||
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
|
||||
)).toBeInTheDocument();
|
||||
|
||||
const textRadio = screen.getByRole('radio', { name: /Text/i });
|
||||
userEvent.click(textRadio);
|
||||
expect(textRadio).toBeChecked();
|
||||
|
||||
const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
|
||||
expect(selectBtn).toBeInTheDocument();
|
||||
|
||||
userEvent.click(selectBtn);
|
||||
});
|
||||
|
||||
expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
|
||||
});
|
||||
|
||||
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
|
||||
const { getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||
let units = null;
|
||||
@@ -889,13 +400,12 @@ describe('<CourseUnit />', () => {
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
|
||||
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||
userEvent.click(editTitleButton);
|
||||
fireEvent.click(editTitleButton);
|
||||
|
||||
const titleEditField = within(unitHeaderTitle).getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
fireEvent.change(titleEditField, { target: { value: newDisplayName } });
|
||||
|
||||
await userEvent.clear(titleEditField);
|
||||
await userEvent.type(titleEditField, newDisplayName);
|
||||
await userEvent.tab();
|
||||
await act(async () => fireEvent.blur(titleEditField));
|
||||
|
||||
await waitFor(async () => {
|
||||
const units = getAllByTestId('course-unit-btn');
|
||||
@@ -1046,6 +556,76 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('checks whether xblock is deleted when corresponding delete button is clicked', async () => {
|
||||
axiosMock
|
||||
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
|
||||
.replyOnce(200, { dummy: 'value' });
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getAllByLabelText,
|
||||
getByRole,
|
||||
getAllByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
|
||||
userEvent.click(xblockActionBtn);
|
||||
|
||||
const deleteBtn = getByRole('button', { name: courseXBlockMessages.blockLabelButtonDelete.defaultMessage });
|
||||
userEvent.click(deleteBtn);
|
||||
expect(getByText(/Delete this component?/)).toBeInTheDocument();
|
||||
|
||||
const deleteConfirmBtn = getByRole('button', { name: deleteModalMessages.deleteButton.defaultMessage });
|
||||
userEvent.click(deleteConfirmBtn);
|
||||
|
||||
expect(getAllByTestId('course-xblock')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('checks whether xblock is duplicate when corresponding delete button is clicked', async () => {
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({
|
||||
parent_locator: blockId,
|
||||
duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id,
|
||||
}))
|
||||
.replyOnce(200, { locator: '1234567890' });
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseVerticalChildrenMock,
|
||||
children: [
|
||||
...courseVerticalChildrenMock.children,
|
||||
{
|
||||
name: 'New Cloned XBlock',
|
||||
block_id: '1234567890',
|
||||
block_type: 'drag-and-drop-v2',
|
||||
user_partition_info: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getAllByLabelText,
|
||||
getAllByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
|
||||
userEvent.click(xblockActionBtn);
|
||||
|
||||
const duplicateBtn = getByText(courseXBlockMessages.blockLabelButtonDuplicate.defaultMessage);
|
||||
userEvent.click(duplicateBtn);
|
||||
|
||||
expect(getAllByTestId('course-xblock')).toHaveLength(3);
|
||||
expect(getByText('New Cloned XBlock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
@@ -1212,6 +792,189 @@ describe('<CourseUnit />', () => {
|
||||
expect(discardChangesBtn).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getAllByLabelText,
|
||||
getByRole,
|
||||
getAllByTestId,
|
||||
queryByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.makePublic,
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
axiosMock
|
||||
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
|
||||
.replyOnce(200, { dummy: 'value' });
|
||||
|
||||
await executeThunk(deleteUnitItemQuery(courseId, blockId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
|
||||
userEvent.click(xblockActionBtn);
|
||||
|
||||
const deleteBtn = getByRole('button', { name: courseXBlockMessages.blockLabelButtonDelete.defaultMessage });
|
||||
userEvent.click(deleteBtn);
|
||||
expect(getByText(/Delete this component?/)).toBeInTheDocument();
|
||||
|
||||
const deleteConfirmBtn = getByRole('button', { name: deleteModalMessages.deleteButton.defaultMessage });
|
||||
userEvent.click(deleteConfirmBtn);
|
||||
|
||||
expect(getAllByTestId('course-xblock')).toHaveLength(1);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({
|
||||
parent_locator: blockId,
|
||||
duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id,
|
||||
}))
|
||||
.replyOnce(200, { locator: '1234567890' });
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseVerticalChildrenMock,
|
||||
children: [
|
||||
...courseVerticalChildrenMock.children,
|
||||
{
|
||||
...courseVerticalChildrenMock.children[0],
|
||||
name: 'New Cloned XBlock',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getAllByLabelText,
|
||||
getAllByTestId,
|
||||
queryByRole,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.makePublic,
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
|
||||
userEvent.click(xblockActionBtn);
|
||||
|
||||
const duplicateBtn = getByText(courseXBlockMessages.blockLabelButtonDuplicate.defaultMessage);
|
||||
userEvent.click(duplicateBtn);
|
||||
|
||||
expect(getAllByTestId('course-xblock')).toHaveLength(3);
|
||||
expect(getByText('New Cloned XBlock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
@@ -1257,7 +1020,7 @@ describe('<CourseUnit />', () => {
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(courseUnitIndexMock.id), {
|
||||
publish: null,
|
||||
metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true },
|
||||
metadata: { visible_to_staff_only: true, group_access: { 50: [2] } },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
@@ -1300,6 +1063,159 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
describe('Copy paste functionality', () => {
|
||||
it('should display "Copy Unit" action button after enabling copy-paste units', async () => {
|
||||
const { queryByText, queryByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText(sidebarMessages.actionButtonCopyUnitTitle.defaultMessage)).toBeNull();
|
||||
expect(queryByRole('button', { name: messages.pasteButtonText.defaultMessage })).toBeNull();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display clipboard information in popover when hovering over What\'s in clipboard text', async () => {
|
||||
const {
|
||||
queryByTestId, getByRole, getAllByLabelText, getByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
|
||||
userEvent.click(xblockActionBtn);
|
||||
userEvent.click(getByRole('button', { name: courseXBlockMessages.blockLabelButtonCopyToClipboard.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardXBlock,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
expect(getByRole('button', { name: messages.pasteButtonText.defaultMessage })).toBeInTheDocument();
|
||||
|
||||
const whatsInClipboardText = getByText(
|
||||
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
|
||||
);
|
||||
|
||||
userEvent.hover(whatsInClipboardText);
|
||||
|
||||
const popoverContent = queryByTestId('popover-content');
|
||||
expect(popoverContent.tagName).toBe('A');
|
||||
expect(popoverContent).toHaveAttribute('href', clipboardXBlock.sourceEditUrl);
|
||||
expect(within(popoverContent).getByText(clipboardXBlock.content.displayName)).toBeInTheDocument();
|
||||
expect(within(popoverContent).getByText(clipboardXBlock.sourceContextTitle)).toBeInTheDocument();
|
||||
expect(within(popoverContent).getByText(clipboardXBlock.content.blockTypeDisplay)).toBeInTheDocument();
|
||||
|
||||
fireEvent.blur(whatsInClipboardText);
|
||||
await waitFor(() => expect(queryByTestId('popover-content')).toBeNull());
|
||||
|
||||
fireEvent.focus(whatsInClipboardText);
|
||||
await waitFor(() => expect(queryByTestId('popover-content')).toBeInTheDocument());
|
||||
|
||||
fireEvent.mouseLeave(whatsInClipboardText);
|
||||
await waitFor(() => expect(queryByTestId('popover-content')).toBeNull());
|
||||
|
||||
fireEvent.mouseEnter(whatsInClipboardText);
|
||||
await waitFor(() => expect(queryByTestId('popover-content')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
|
||||
const {
|
||||
getAllByTestId, getByRole, getAllByLabelText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
|
||||
userEvent.click(xblockActionBtn);
|
||||
userEvent.click(getByRole('button', { name: courseXBlockMessages.blockLabelButtonCopyToClipboard.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardXBlock,
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getAllByTestId('course-xblock')).toHaveLength(2);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseVerticalChildrenMock,
|
||||
children: [
|
||||
...courseVerticalChildrenMock.children,
|
||||
{
|
||||
name: 'Copy XBlock',
|
||||
block_id: '1234567890',
|
||||
block_type: 'drag-and-drop-v2',
|
||||
user_partition_info: {
|
||||
selectable_partitions: [],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
expect(getAllByTestId('course-xblock')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should display the "Paste component" button after copying a xblock to clipboard', async () => {
|
||||
const { getByRole, getAllByLabelText } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
|
||||
userEvent.click(xblockActionBtn);
|
||||
userEvent.click(getByRole('button', { name: courseXBlockMessages.blockLabelButtonCopyToClipboard.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardXBlock,
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
expect(getByRole('button', { name: messages.pasteButtonText.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
|
||||
const {
|
||||
getAllByTestId, getByRole,
|
||||
@@ -1360,77 +1276,6 @@ describe('<CourseUnit />', () => {
|
||||
.toHaveBeenCalledWith(`/course/${courseId}/container/${blockId}/${updatedAncestorsChild.id}`, { replace: true });
|
||||
});
|
||||
|
||||
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
|
||||
const { getByRole, getByTitle } = render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.copyXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardXBlock,
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length),
|
||||
);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.copyXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
});
|
||||
|
||||
const updatedCourseVerticalChildren = [
|
||||
...courseVerticalChildrenMock.children,
|
||||
{
|
||||
name: 'Copy XBlock',
|
||||
block_id: '1234567890',
|
||||
block_type: 'drag-and-drop-v2',
|
||||
user_partition_info: {
|
||||
selectable_partitions: [],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseVerticalChildrenMock,
|
||||
children: updatedCourseVerticalChildren,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a notification about new files after pasting a component', async () => {
|
||||
const {
|
||||
queryByTestId, getByTestId, getByRole,
|
||||
@@ -1468,7 +1313,9 @@ describe('<CourseUnit />', () => {
|
||||
.reply(200, clipboardMockResponse);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, updatedCourseSectionVerticalData);
|
||||
.reply(200, {
|
||||
...updatedCourseSectionVerticalData,
|
||||
});
|
||||
|
||||
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
@@ -1628,454 +1475,55 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Move functionality', () => {
|
||||
const requestData = {
|
||||
sourceLocator: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
targetParentLocator: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
||||
title: 'Getting Started',
|
||||
currentParentLocator: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
isMoving: true,
|
||||
callbackFn: jest.fn(),
|
||||
};
|
||||
const messageEvent = new MessageEvent('message', {
|
||||
data: {
|
||||
type: messageTypes.showMoveXBlockModal,
|
||||
payload: {
|
||||
sourceXBlockInfo: {
|
||||
id: requestData.sourceLocator,
|
||||
displayName: requestData.title,
|
||||
},
|
||||
sourceParentXBlockInfo: {
|
||||
id: requestData.currentParentLocator,
|
||||
category: 'vertical',
|
||||
hasChildren: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
origin: '*',
|
||||
});
|
||||
describe('Drag and drop', () => {
|
||||
it('checks xblock list is restored to original order when API call fails', async () => {
|
||||
const { findAllByRole } = render(<RootWrapper />);
|
||||
|
||||
it('should display "Move Modal" on receive trigger message', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
|
||||
window.dispatchEvent(messageEvent);
|
||||
});
|
||||
|
||||
expect(getByText(
|
||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigates to xBlock current unit', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
|
||||
window.dispatchEvent(messageEvent);
|
||||
});
|
||||
|
||||
expect(getByText(
|
||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
|
||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||
const currentSectionItemBtn = getByRole('button', {
|
||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||
userEvent.click(currentSectionItemBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
const currentSubsection = currentSection.child_info.children[0];
|
||||
const currentSubsectionItemBtn = getByRole('button', {
|
||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||
userEvent.click(currentSubsectionItemBtn);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const currentComponentLocationText = getByText(
|
||||
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
|
||||
);
|
||||
expect(currentComponentLocationText).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow move operation and handles it successfully', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
const xBlocksDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = xBlocksDraggers[1];
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
.reply(200, {});
|
||||
.onPut(getXBlockBaseApiUrl(blockId))
|
||||
.reply(500, { dummy: 'value' });
|
||||
|
||||
const xBlock1 = store.getState().courseUnit.courseVerticalChildren.children[0].id;
|
||||
|
||||
fireEvent.keyDown(draggableButton, { key: 'ArrowUp' });
|
||||
|
||||
await waitFor(async () => {
|
||||
fireEvent.keyDown(draggableButton, { code: 'Space' });
|
||||
|
||||
const saveStatus = store.getState().courseUnit.savingStatus;
|
||||
expect(saveStatus).toEqual(RequestStatus.FAILED);
|
||||
});
|
||||
|
||||
const xBlock1New = store.getState().courseUnit.courseVerticalChildren.children[0].id;
|
||||
expect(xBlock1).toBe(xBlock1New);
|
||||
});
|
||||
|
||||
it('check that new xblock list is saved when dragged', async () => {
|
||||
const { findAllByRole } = render(<RootWrapper />);
|
||||
|
||||
const xBlocksDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = xBlocksDraggers[1];
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
|
||||
window.dispatchEvent(messageEvent);
|
||||
});
|
||||
|
||||
expect(getByText(
|
||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
|
||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||
const currentSectionItemBtn = getByRole('button', {
|
||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||
userEvent.click(currentSectionItemBtn);
|
||||
|
||||
const currentSubsection = currentSection.child_info.children[1];
|
||||
await waitFor(() => {
|
||||
const currentSubsectionItemBtn = getByRole('button', {
|
||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||
userEvent.click(currentSubsectionItemBtn);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const currentUnit = currentSubsection.child_info.children[0];
|
||||
const currentUnitItemBtn = getByRole('button', {
|
||||
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentUnitItemBtn).toBeInTheDocument();
|
||||
userEvent.click(currentUnitItemBtn);
|
||||
});
|
||||
|
||||
const moveModalBtn = getByRole('button', {
|
||||
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
|
||||
});
|
||||
expect(moveModalBtn).toBeInTheDocument();
|
||||
expect(moveModalBtn).not.toBeDisabled();
|
||||
userEvent.click(moveModalBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
|
||||
expect(window.scrollTo).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
|
||||
const {
|
||||
queryByRole,
|
||||
getByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
.reply(200, {});
|
||||
|
||||
await executeThunk(patchUnitItemQuery({
|
||||
sourceLocator: requestData.sourceLocator,
|
||||
targetParentLocator: requestData.targetParentLocator,
|
||||
title: requestData.title,
|
||||
currentParentLocator: requestData.currentParentLocator,
|
||||
isMoving: requestData.isMoving,
|
||||
callbackFn: requestData.callbackFn,
|
||||
}), store.dispatch);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
|
||||
|
||||
const dismissButton = queryByRole('button', {
|
||||
name: /dismiss/i, hidden: true,
|
||||
});
|
||||
const undoButton = queryByRole('button', {
|
||||
name: messages.undoMoveButton.defaultMessage, hidden: true,
|
||||
});
|
||||
const newLocationButton = queryByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
|
||||
expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
|
||||
expect(dismissButton).toBeInTheDocument();
|
||||
expect(undoButton).toBeInTheDocument();
|
||||
expect(newLocationButton).toBeInTheDocument();
|
||||
|
||||
userEvent.click(undoButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
expect(getByText(
|
||||
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
expect(dismissButton).toBeInTheDocument();
|
||||
expect(undoButton).not.toBeInTheDocument();
|
||||
expect(newLocationButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to new location by button click', async () => {
|
||||
const {
|
||||
queryByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
.reply(200, {});
|
||||
|
||||
await executeThunk(patchUnitItemQuery({
|
||||
sourceLocator: requestData.sourceLocator,
|
||||
targetParentLocator: requestData.targetParentLocator,
|
||||
title: requestData.title,
|
||||
currentParentLocator: requestData.currentParentLocator,
|
||||
isMoving: requestData.isMoving,
|
||||
callbackFn: requestData.callbackFn,
|
||||
}), store.dispatch);
|
||||
|
||||
const newLocationButton = queryByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
userEvent.click(newLocationButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(
|
||||
`/course/${courseId}/container/${blockId}/${requestData.currentParentLocator}`,
|
||||
{ replace: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('XBlock restrict access', () => {
|
||||
it('opens xblock restrict access modal successfully', () => {
|
||||
const {
|
||||
getByTitle, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
|
||||
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
|
||||
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
|
||||
|
||||
waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const usageId = courseVerticalChildrenMock.children[0].block_id;
|
||||
expect(iframe).toBeInTheDocument();
|
||||
|
||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||
usageId,
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
|
||||
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
|
||||
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
|
||||
expect(within(configureModal).getByRole('button', { name: modalSaveBtnText })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes xblock restrict access modal when cancel button is clicked', async () => {
|
||||
const {
|
||||
getByTitle, queryByTestId, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
userEvent.click(within(configureModal).getByRole('button', {
|
||||
name: configureModalMessages.cancelButton.defaultMessage,
|
||||
}));
|
||||
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles submit xblock restrict access data when save button is clicked', async () => {
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(id), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: { visible_to_staff_only: false, group_access: { 970807507: [1959537066] } },
|
||||
})
|
||||
.onPut(getXBlockBaseApiUrl(blockId))
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const {
|
||||
getByTitle, getByRole, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
const xBlock1 = store.getState().courseUnit.courseVerticalChildren.children[0].id;
|
||||
|
||||
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
|
||||
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
|
||||
fireEvent.keyDown(draggableButton, { key: 'ArrowUp' });
|
||||
|
||||
waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
await waitFor(async () => {
|
||||
fireEvent.keyDown(draggableButton, { code: 'Space' });
|
||||
|
||||
const saveStatus = store.getState().courseUnit.savingStatus;
|
||||
expect(saveStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
|
||||
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
|
||||
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
|
||||
|
||||
const restrictAccessSelect = getByRole('combobox', {
|
||||
name: configureModalMessages.restrictAccessTo.defaultMessage,
|
||||
});
|
||||
|
||||
userEvent.selectOptions(restrictAccessSelect, '0');
|
||||
|
||||
// eslint-disable-next-line array-callback-return
|
||||
userPartitionInfoFormatted.selectablePartitions[0].groups.map((group) => {
|
||||
expect(within(configureModal).getByRole('checkbox', { name: group.name })).not.toBeChecked();
|
||||
expect(within(configureModal).queryByText(group.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const group1Checkbox = within(configureModal).getByRole('checkbox', { name: accessGroupName1 });
|
||||
userEvent.click(group1Checkbox);
|
||||
expect(group1Checkbox).toBeChecked();
|
||||
|
||||
const saveModalBtnText = within(configureModal).getByRole('button', {
|
||||
name: configureModalMessages.saveButton.defaultMessage,
|
||||
});
|
||||
expect(saveModalBtnText).toBeInTheDocument();
|
||||
|
||||
userEvent.click(saveModalBtnText);
|
||||
expect(handleConfigureSubmitMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
|
||||
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
|
||||
|
||||
updatedCourseVerticalChildrenMock.children = updatedCourseVerticalChildrenMock.children
|
||||
.map((child) => (child.block_id === targetBlockId
|
||||
? { ...child, block_type: 'html' }
|
||||
: child));
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, updatedCourseVerticalChildrenMock);
|
||||
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.currentXBlockId, {
|
||||
id: targetBlockId,
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {});
|
||||
simulatePostMessageEvent(messageTypes.newXBlockEditor, {});
|
||||
expect(mockedUsedNavigate)
|
||||
.toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Library Content page', () => {
|
||||
const newUnitId = '12345';
|
||||
const sequenceId = courseSectionVerticalMock.subsection_location;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock: {
|
||||
...courseSectionVerticalMock.xblock,
|
||||
category: 'library_content',
|
||||
},
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
category: 'library_content',
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
});
|
||||
|
||||
it('navigates to library content page on receive window event', () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId });
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
|
||||
});
|
||||
|
||||
it('should render library content page correctly', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
queryByRole,
|
||||
getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||
|
||||
expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
|
||||
expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
|
||||
});
|
||||
const xBlock2 = store.getState().courseUnit.courseVerticalChildren.children[1].id;
|
||||
expect(xBlock1).toBe(xBlock2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,1683 +0,0 @@
|
||||
module.exports = {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
||||
display_name: 'Demonstration Course',
|
||||
category: 'course',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
unit_level_discussions: false,
|
||||
child_info: {
|
||||
category: 'chapter',
|
||||
display_name: 'Section',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
|
||||
display_name: 'Introduction',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
|
||||
display_name: 'Demo Course Overview',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
|
||||
display_name: 'Introduction: Video and Sequences',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
|
||||
display_name: 'Blank HTML Page',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f7cc083ff66d442eafafd48152881276',
|
||||
display_name: '“Blank HTML Page”的副本',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd',
|
||||
display_name: 'Welcome!',
|
||||
category: 'video',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@6e72ebc448694e42ac56553af74304e7',
|
||||
display_name: 'Video',
|
||||
category: 'video',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@8c964a36521a42e3a221e7b8cf6c94fc',
|
||||
display_name: 'Subsection',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
display_name: 'Example Week 1: Getting Started',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
display_name: 'Lesson 1 - Getting Started',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
display_name: 'Getting Started',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@82d599b014b246c7a9b5dfc750dc08a9',
|
||||
display_name: 'Getting Started',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
display_name: 'Working with Videos',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6bcccc2d7343416e9e03fd7325b2f232',
|
||||
display_name: '',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@7e9b434e6de3435ab99bd3fb25bde807',
|
||||
display_name: 'A Shared Culture',
|
||||
category: 'video',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@412dc8dbb6674014862237b23c1f643f',
|
||||
display_name: 'Working with Videos',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
display_name: 'Videos on edX',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@0a3b4139f51a4917a3aff9d519b1eeb6',
|
||||
display_name: 'Videos on edX',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@5c90cffecd9b48b188cbfea176bf7fe9',
|
||||
display_name: 'Video',
|
||||
category: 'video',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@722085be27c84ac693cfebc8ac5da700',
|
||||
display_name: 'Videos on edX',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
display_name: 'Video Demonstrations',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@ed5dccf14ae94353961f46fa07217491',
|
||||
display_name: '',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@9f9e1373cc8243b985c8750cc8acec7d',
|
||||
display_name: 'Video Demonstrations',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
display_name: 'Video Presentation Styles',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@c2f7008c9ccf4bd09d5d800c98fb0722',
|
||||
display_name: '',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6',
|
||||
display_name: 'Connecting a Circuit and a Circuit Diagram',
|
||||
category: 'video',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e2cb0e0994f84b0abfa5f4ae42ed9d44',
|
||||
display_name: 'Video Presentation Styles',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
display_name: 'Interactive Questions',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618',
|
||||
display_name: 'Interactive Questions',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@3169f89efde2452993f2f2d9bc74f5b2',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
|
||||
display_name: 'Exciting Labs and Tools',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@ffcd6351126d4ca984409180e41d1b51',
|
||||
display_name: 'Exciting Labs and Tools',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1c8d47c425724346a7968fa1bc745dcd',
|
||||
display_name: 'Labs and Tools',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
display_name: 'Reading Assignments',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@e0254b911fa246218bd98bbdadffef06',
|
||||
display_name: 'Reading Assignments',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@2574c523e97b477a9d72fbb37bfb995f',
|
||||
display_name: 'Text',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@932e6f2ce8274072a355a94560216d1a',
|
||||
display_name: 'Perchance to Dream',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@303034da25524878a2e66fb57c91cf85',
|
||||
display_name: 'Attributing Blame',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ffa5817d49e14fec83ad6187cbe16358',
|
||||
display_name: 'Reading Sample',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
|
||||
display_name: 'When Are Your Exams? ',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf',
|
||||
display_name: 'When Are Your Exams? ',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
display_name: 'Homework - Question Styles',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
|
||||
display_name: 'Pointing on a Picture',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c',
|
||||
display_name: 'Pointing on a Picture Component',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e5eac7e1a5a24f5fa7ed77bb6d136591',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
|
||||
display_name: 'Drag and Drop',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@d2e35c1d294b4ba0b3b1048615605d2a',
|
||||
display_name: 'Drag and Drop',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@5ab88e67d46049b9aa694cb240c39cef',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
|
||||
display_name: 'Multiple Choice Questions',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4',
|
||||
display_name: 'Multiple Choice Questions',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@67c26b1e826e47aaa29757f62bcd1ad0',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
|
||||
display_name: 'Mathematical Expressions',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@Sample_Algebraic_Problem',
|
||||
display_name: 'Mathematical Expressions',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@870371212ba04dcf9536d7c7b8f3109e',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
|
||||
display_name: 'Chemical Equations',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@Sample_ChemFormula_Problem',
|
||||
display_name: 'Chemical Equations',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4d672c5893cb4f1dad0de67d2008522e',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
|
||||
display_name: 'Numerical Input',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974',
|
||||
display_name: 'Numerical Input',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@501aed9d902349eeb2191fa505548de2',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
|
||||
display_name: 'Text input',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02',
|
||||
display_name: 'Text Input',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6244918637ed4ff4b5f94a840a7e4b43',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb6b62dbec4348528629cf2232b86aea',
|
||||
display_name: 'Instructor Programmed Responses',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions',
|
||||
display_name: 'Example Week 2: Get Interactive',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations',
|
||||
display_name: "Lesson 2 - Let's Get Interactive!",
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0',
|
||||
display_name: "Lesson 2 - Let's Get Interactive! ",
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@78d7d3642f3a4dbabbd1b017861aa5f2',
|
||||
display_name: "Lesson 2: Let's Get Interactive!",
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e',
|
||||
display_name: 'An Interactive Reference Table',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@html_07d547513285',
|
||||
display_name: 'An Interactive Reference Table',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6f7a6670f87147149caeff6afa07a526',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471',
|
||||
display_name: 'Zooming Diagrams',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@700x_pathways',
|
||||
display_name: 'Zooming Diagrams',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e0d7423118ab432582d03e8e8dad8e36',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c',
|
||||
display_name: 'Electronic Sound Experiment',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@Lab_5B_Mosfet_Amplifier_Experiment',
|
||||
display_name: 'Electronic Sound Experiment',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@03f051f9a8814881a3783d2511613aa6',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d',
|
||||
display_name: 'New Unit',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@af7fe1335eb841cd81ce31c7ee8eb069',
|
||||
display_name: 'Video',
|
||||
category: 'video',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations',
|
||||
display_name: 'Homework - Labs and Demos',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6cee45205a449369d7ef8f159b22bdf',
|
||||
display_name: 'Labs and Demos',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@2bee8c4248e842a19ba1e73ed8d426c2',
|
||||
display_name: 'Labs and Demos',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55',
|
||||
display_name: 'Code Grader',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@891211e17f9a472290a5f12c7a6626d7',
|
||||
display_name: 'Code Grader',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader',
|
||||
display_name: 'problem',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@c6cd4bea43454aaea60ad01beb0cf213',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1',
|
||||
display_name: 'Electric Circuit Simulator',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@d5a5caaf35e84ebc9a747038465dcfb4',
|
||||
display_name: 'Electronic Circuit Simulator',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@free_form_simulation',
|
||||
display_name: 'problem',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@logic_gate_problem',
|
||||
display_name: 'problem',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4f06b358a96f4d1dae57d6d81acd06f2',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae',
|
||||
display_name: 'Protein Creator',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@78e3719e864e45f3bee938461f3c3de6',
|
||||
display_name: 'Protein Builder',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@700x_proteinmake',
|
||||
display_name: 'Designing Proteins in Two Dimensions',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ed01bcd164e64038a78964a16eac3edc',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7',
|
||||
display_name: 'Molecule Structures',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@9b9687073e904ae197799dc415df899f',
|
||||
display_name: 'Molecule Structures',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e',
|
||||
display_name: 'Homework - Essays',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9050',
|
||||
display_name: 'Peer Assessed Essays',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@openassessment+block@b24c33ea35954c7889e1d2944d3fe397',
|
||||
display_name: 'Open Response Assessment',
|
||||
category: 'openassessment',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@12ad4f3ff4c14114a6e629b00e000976',
|
||||
display_name: 'Peer Grading',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration',
|
||||
display_name: 'Example Week 3: Be Social',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e',
|
||||
display_name: 'Lesson 3 - Be Social',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3c4b575924bf4b75a2f3542df5c354fc',
|
||||
display_name: 'Be Social',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0',
|
||||
display_name: 'Be Social',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_3888db0bc286',
|
||||
display_name: 'Discussion Forums',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7',
|
||||
display_name: 'Discussion Forums',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@discussion_5deb6081620d',
|
||||
display_name: 'Discussion Forums',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@312cb4faed17420e82ab3178fc3e251a',
|
||||
display_name: 'Getting Help',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@8bb218cccf8d40519a971ff0e4901ccf',
|
||||
display_name: 'Getting Help',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@7efc7bf4a47b4a6cb6595c32cde7712a',
|
||||
display_name: 'Homework - Find Your Study Buddy',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339',
|
||||
display_name: 'Blank HTML Page',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855',
|
||||
display_name: 'Homework - Find Your Study Buddy',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@26d89b08f75d48829a63520ed8b0037d',
|
||||
display_name: 'Homework - Find Your Study Buddy',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5',
|
||||
display_name: 'Find Your Study Buddy',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa',
|
||||
display_name: 'More Ways to Connect',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3f2c11aba9434e459676a7d7acc4d960',
|
||||
display_name: 'Google Hangout',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@d45779ad3d024a40a09ad8cc317c0970',
|
||||
display_name: 'Text',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@55cbc99f262443d886a25cf84594eafb',
|
||||
display_name: 'Text',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ade92343df3d4953a40ab3adc8805390',
|
||||
display_name: 'Google Hangout',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7',
|
||||
display_name: 'About Exams and Certificates',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow',
|
||||
display_name: 'edX Exams',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@934cc32c177d41b580c8413e561346b3',
|
||||
display_name: 'EdX Exams',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530',
|
||||
display_name: 'EdX Exams',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131',
|
||||
display_name: 'Immediate Feedback',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@ex_practice_2',
|
||||
display_name: 'Immediate Feedback',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4aba537a78774bd5a862485a8563c345',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93',
|
||||
display_name: 'Getting Answers',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4',
|
||||
display_name: 'Getting Answers',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@f480df4ce91347c5ae4301ddf6146238',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa',
|
||||
display_name: 'Answering More Than Once',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@651e0945b77f42e0a4c89b8c3e6f5b3b',
|
||||
display_name: 'Answering More Than Once',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@b8cec2a19ebf463f90cd3544c7927b0e',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91',
|
||||
display_name: 'Limited Checks',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@ex_practice_limited_checks',
|
||||
display_name: 'Limited Checks',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@d1b84dcd39b0423d9e288f27f0f7f242',
|
||||
display_name: 'Few Checks',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@cd177caa62444fbca48aa8f843f09eac',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a',
|
||||
display_name: 'Randomized Questions',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@ex_practice_3',
|
||||
display_name: 'Randomized Questions',
|
||||
category: 'problem',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ddede76df71045ffa16de9d1481d2119',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c',
|
||||
display_name: 'Overall Grade Performance',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c',
|
||||
display_name: 'Overall Grade',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1a810b1a3b2447b998f0917d0e5a802b',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff',
|
||||
display_name: 'Passing a Course',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9',
|
||||
display_name: 'Passing a Course',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@23e6eda482c04335af2bb265beacaf59',
|
||||
display_name: '',
|
||||
category: 'discussion',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45',
|
||||
display_name: 'Getting Your edX Certificate',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@148ae8fa73ea460eb6f05505da0ba6e6',
|
||||
display_name: 'Getting Your edX Certificate',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6b6bee43c7c641509da71c9299cc9f5a',
|
||||
display_name: 'Blank HTML Page',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@59666313a79946079f5ef4fff36e45f0',
|
||||
display_name: 'IFrame',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@f9fd819dfb224d118e4df4d46c648179',
|
||||
display_name: 'Subsection',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c8165538b5f04283879efc8e8deb2d92',
|
||||
display_name: 'Iframe',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@fd3d0a72d0d344af9a53de144d83af1f',
|
||||
display_name: 'IFrame Tool',
|
||||
category: 'html',
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@a7deaeb85ee24470871c912536534a59',
|
||||
display_name: 'Subsection',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -82,7 +82,6 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'discussion',
|
||||
@@ -102,7 +101,6 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'library',
|
||||
@@ -116,13 +114,12 @@ module.exports = {
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Legacy Library Content',
|
||||
display_name: 'Library Content',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
@@ -182,7 +179,6 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'openassessment',
|
||||
@@ -234,7 +230,6 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'problem',
|
||||
@@ -254,7 +249,6 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
@@ -274,7 +268,6 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'drag-and-drop-v2',
|
||||
@@ -294,47 +287,6 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'library_v2',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Library Content',
|
||||
category: 'library_v2',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Library Content',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
type: 'itembank',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Problem Bank',
|
||||
category: 'itembank',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Problem Bank',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: true,
|
||||
},
|
||||
],
|
||||
course_sequence_ids: [
|
||||
|
||||
@@ -4,4 +4,3 @@ export { default as courseUnitMock } from './courseUnit';
|
||||
export { default as courseCreateXblockMock } from './courseCreateXblock';
|
||||
export { default as courseVerticalChildrenMock } from './courseVerticalChildren';
|
||||
export { default as clipboardMockResponse } from './clipboardResponse';
|
||||
export { default as courseOutlineInfoMock } from './courseOutlineInfo';
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Button, StandardModal, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { getCourseSectionVertical } from '../data/selectors';
|
||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||
import ComponentModalView from './add-component-modals/ComponentModalView';
|
||||
import AddComponentButton from './add-component-btn';
|
||||
import messages from './messages';
|
||||
import { ComponentPicker } from '../../library-authoring/component-picker';
|
||||
import { messageTypes } from '../constants';
|
||||
import { useIframe } from '../context/hooks';
|
||||
import { useEventListener } from '../../generic/hooks';
|
||||
|
||||
const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -23,34 +16,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
|
||||
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
|
||||
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
|
||||
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
|
||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
|
||||
const [selectedComponents, setSelectedComponents] = useState([]);
|
||||
const { sendMessageToIframe } = useIframe();
|
||||
|
||||
const receiveMessage = useCallback(({ data: { type } }) => {
|
||||
if (type === messageTypes.showMultipleComponentPicker) {
|
||||
showSelectLibraryContentModal();
|
||||
}
|
||||
}, [showSelectLibraryContentModal]);
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
const onComponentSelectionSubmit = useCallback(() => {
|
||||
sendMessageToIframe(messageTypes.addSelectedComponentsToBank, { selectedComponents });
|
||||
closeSelectLibraryContentModal();
|
||||
}, [selectedComponents]);
|
||||
|
||||
const handleLibraryV2Selection = useCallback((selection) => {
|
||||
handleCreateNewCourseXBlock({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
category: selection.blockType,
|
||||
parentLocator: blockId,
|
||||
libraryContentKey: selection.usageKey,
|
||||
});
|
||||
closeAddLibraryContentModal();
|
||||
}, []);
|
||||
const { componentTemplates } = useSelector(getCourseSectionVertical);
|
||||
|
||||
const handleCreateNewXBlock = (type, moduleName) => {
|
||||
switch (type) {
|
||||
@@ -61,7 +27,6 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
case COMPONENT_TYPES.problem:
|
||||
case COMPONENT_TYPES.video:
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
|
||||
});
|
||||
break;
|
||||
@@ -70,12 +35,6 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
case COMPONENT_TYPES.library:
|
||||
handleCreateNewCourseXBlock({ type, category: 'library_content', parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.itembank:
|
||||
handleCreateNewCourseXBlock({ type, category: 'itembank', parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.libraryV2:
|
||||
showAddLibraryContentModal();
|
||||
break;
|
||||
case COMPONENT_TYPES.advanced:
|
||||
handleCreateNewCourseXBlock({
|
||||
type: moduleName, category: moduleName, parentLocator: blockId,
|
||||
@@ -92,7 +51,6 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
boilerplate: moduleName,
|
||||
parentLocator: blockId,
|
||||
}, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/html/${locator}`);
|
||||
});
|
||||
break;
|
||||
@@ -109,7 +67,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
|
||||
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
|
||||
{componentTemplates.map((component) => {
|
||||
const { type, displayName, beta } = component;
|
||||
const { type, displayName } = component;
|
||||
let modalParams;
|
||||
|
||||
if (!component.templates.length) {
|
||||
@@ -145,7 +103,6 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
onClick={() => handleCreateNewXBlock(type)}
|
||||
displayName={displayName}
|
||||
type={type}
|
||||
beta={beta}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
@@ -161,36 +118,6 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<StandardModal
|
||||
title={
|
||||
isAddLibraryContentModalOpen
|
||||
? intl.formatMessage(messages.singleComponentPickerModalTitle)
|
||||
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
|
||||
}
|
||||
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
|
||||
onClose={() => {
|
||||
closeAddLibraryContentModal();
|
||||
closeSelectLibraryContentModal();
|
||||
}}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
footerNode={
|
||||
isSelectLibraryContentModalOpen && (
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onComponentSelectionSubmit}>
|
||||
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ComponentPicker
|
||||
showOnlyPublished
|
||||
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
|
||||
onComponentSelected={handleLibraryV2Selection}
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
/>
|
||||
</StandardModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import {
|
||||
act, render, screen, waitFor, within,
|
||||
render, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
@@ -18,56 +17,20 @@ import { courseSectionVerticalMock } from '../__mocks__';
|
||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||
import AddComponent from './AddComponent';
|
||||
import messages from './messages';
|
||||
import { IframeProvider } from '../context/iFrameContext';
|
||||
import { messageTypes } from '../constants';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
const blockId = '123';
|
||||
const handleCreateNewCourseXBlockMock = jest.fn();
|
||||
const usageKey = 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fddest-usage-key';
|
||||
|
||||
// Mock ComponentPicker to call onComponentSelected on click
|
||||
jest.mock('../../library-authoring/component-picker', () => ({
|
||||
ComponentPicker: (props) => {
|
||||
const onClick = () => {
|
||||
if (props.componentPickerMode === 'single') {
|
||||
props.onComponentSelected({
|
||||
usageKey,
|
||||
blockType: 'html',
|
||||
});
|
||||
} else {
|
||||
props.onChangeComponentSelection([{
|
||||
usageKey,
|
||||
blockType: 'html',
|
||||
}]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<button type="submit" onClick={onClick}>
|
||||
Dummy button
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSendMessageToIframe = jest.fn();
|
||||
jest.mock('../context/hooks', () => ({
|
||||
useIframe: () => ({
|
||||
sendMessageToIframe: mockSendMessageToIframe,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<IframeProvider>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
</IframeProvider>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
@@ -96,19 +59,11 @@ describe('<AddComponent />', () => {
|
||||
const componentTemplates = courseSectionVerticalMock.component_templates;
|
||||
|
||||
expect(getByRole('heading', { name: messages.title.defaultMessage })).toBeInTheDocument();
|
||||
Object.keys(componentTemplates).forEach((component) => {
|
||||
const btn = getByRole('button', {
|
||||
name: new RegExp(
|
||||
`${componentTemplates[component].type
|
||||
} ${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`,
|
||||
'i',
|
||||
),
|
||||
});
|
||||
expect(btn).toBeInTheDocument();
|
||||
if (component.beta) {
|
||||
expect(within(btn).queryByText('Beta')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
Object.keys(componentTemplates).map((component) => (
|
||||
expect(getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`, 'i'),
|
||||
})).toBeInTheDocument()
|
||||
));
|
||||
});
|
||||
|
||||
it('AddComponent component doesn\'t render when there aren\'t componentTemplates', async () => {
|
||||
@@ -156,11 +111,7 @@ describe('<AddComponent />', () => {
|
||||
}
|
||||
|
||||
return expect(getByRole('button', {
|
||||
name: new RegExp(
|
||||
`${componentTemplates[component].type
|
||||
} ${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`,
|
||||
'i',
|
||||
),
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`, 'i'),
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -225,7 +176,7 @@ describe('<AddComponent />', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
const discussionButton = getByRole('button', {
|
||||
name: new RegExp(`problem ${messages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(discussionButton);
|
||||
@@ -236,22 +187,6 @@ describe('<AddComponent />', () => {
|
||||
}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('calls handleCreateNewCourseXBlock with correct parameters when Problem bank xblock create button is clicked', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
const problemBankBtn = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Problem Bank`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(problemBankBtn);
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
|
||||
parentLocator: '123',
|
||||
type: COMPONENT_TYPES.itembank,
|
||||
category: 'itembank',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls handleCreateNewCourseXBlock with correct parameters when Video xblock create button is clicked', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
@@ -271,7 +206,7 @@ describe('<AddComponent />', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
const libraryButton = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Legacy Library Content`, 'i'),
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Library Content`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(libraryButton);
|
||||
@@ -444,68 +379,6 @@ describe('<AddComponent />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows library picker on clicking v2 library content btn', async () => {
|
||||
renderComponent();
|
||||
const libBtn = await screen.findByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
|
||||
});
|
||||
userEvent.click(libBtn);
|
||||
|
||||
// click dummy button to execute onComponentSelected prop.
|
||||
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
|
||||
userEvent.click(dummyBtn);
|
||||
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
parentLocator: '123',
|
||||
category: 'html',
|
||||
libraryContentKey: usageKey,
|
||||
});
|
||||
});
|
||||
|
||||
it('closes library component picker on close', async () => {
|
||||
renderComponent();
|
||||
const libBtn = await screen.findByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
|
||||
});
|
||||
userEvent.click(libBtn);
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Dummy button' })).toBeInTheDocument();
|
||||
// click dummy button to execute onComponentSelected prop.
|
||||
const closeBtn = await screen.findByRole('button', { name: 'Close' });
|
||||
userEvent.click(closeBtn);
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Dummy button' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows component picker on window message', async () => {
|
||||
renderComponent();
|
||||
const message = {
|
||||
data: {
|
||||
type: messageTypes.showMultipleComponentPicker,
|
||||
},
|
||||
};
|
||||
// Dispatch showMultipleComponentPicker message event to open the picker modal.
|
||||
act(() => {
|
||||
window.dispatchEvent(new MessageEvent('message', message));
|
||||
});
|
||||
|
||||
// click dummy button to execute onChangeComponentSelection prop.
|
||||
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
|
||||
userEvent.click(dummyBtn);
|
||||
|
||||
const submitBtn = await screen.findByRole('button', { name: 'Add selected components' });
|
||||
userEvent.click(submitBtn);
|
||||
|
||||
expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.addSelectedComponentsToBank, {
|
||||
selectedComponents: [{
|
||||
blockType: 'html',
|
||||
usageKey,
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
describe('component support label', () => {
|
||||
it('component support label is hidden if component support legend is disabled', async () => {
|
||||
const supportLevels = ['fs', 'ps'];
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { EditNote as EditNoteIcon } from '@openedx/paragon/icons';
|
||||
|
||||
import { COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';
|
||||
import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';
|
||||
|
||||
const AddComponentIcon = ({ type }) => {
|
||||
const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon;
|
||||
@@ -11,7 +11,7 @@ const AddComponentIcon = ({ type }) => {
|
||||
};
|
||||
|
||||
AddComponentIcon.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(Object.values(COMPONENT_TYPES)).isRequired,
|
||||
};
|
||||
|
||||
export default AddComponentIcon;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Badge, Button } from '@openedx/paragon';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import AddComponentIcon from './AddComponentIcon';
|
||||
|
||||
const AddComponentButton = ({
|
||||
type, displayName, onClick, beta,
|
||||
}) => {
|
||||
const AddComponentButton = ({ type, displayName, onClick }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
@@ -19,20 +17,14 @@ const AddComponentButton = ({
|
||||
<AddComponentIcon type={type} />
|
||||
<span className="sr-only">{intl.formatMessage(messages.buttonText)}</span>
|
||||
<span className="small mt-2">{displayName}</span>
|
||||
{beta && <Badge className="pb-1 mt-1" variant="primary">Beta</Badge>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
AddComponentButton.defaultProps = {
|
||||
beta: false,
|
||||
};
|
||||
|
||||
AddComponentButton.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
beta: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AddComponentButton;
|
||||
|
||||
@@ -58,11 +58,9 @@ const ComponentModalView = ({
|
||||
const isDisplaySupportLabel = supportLegend.showLegend && supportLabels[componentTemplate.supportLevel];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={componentTemplate.displayName}
|
||||
className="d-flex justify-content-between w-100 mb-2.5 align-items-end"
|
||||
>
|
||||
<div className="d-flex justify-content-between w-100 mb-2.5 align-items-end">
|
||||
<Form.Radio
|
||||
key={componentTemplate.displayName}
|
||||
className="add-component-modal-radio"
|
||||
value={value}
|
||||
>
|
||||
@@ -72,7 +70,7 @@ const ComponentModalView = ({
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={(
|
||||
<Tooltip id={`${componentTemplate.displayName}-support-tooltip`}>
|
||||
<Tooltip>
|
||||
{supportLabels[componentTemplate.supportLevel].tooltip}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -4,42 +4,22 @@ const messages = defineMessages({
|
||||
title: {
|
||||
id: 'course-authoring.course-unit.add.component.title',
|
||||
defaultMessage: 'Add a new component',
|
||||
description: 'Title text for add component section in course unit.',
|
||||
},
|
||||
buttonText: {
|
||||
id: 'course-authoring.course-unit.add.component.button.text',
|
||||
defaultMessage: 'Add Component:',
|
||||
description: 'Information text for screen-readers about each add component button',
|
||||
},
|
||||
modalBtnText: {
|
||||
id: 'course-authoring.course-unit.modal.button.text',
|
||||
defaultMessage: 'Select',
|
||||
description: 'Information text for screen-readers about each add component button',
|
||||
},
|
||||
singleComponentPickerModalTitle: {
|
||||
id: 'course-authoring.course-unit.modal.single-title.text',
|
||||
defaultMessage: 'Select component',
|
||||
description: 'Library content picker modal title.',
|
||||
},
|
||||
multipleComponentPickerModalTitle: {
|
||||
id: 'course-authoring.course-unit.modal.multiple-title.text',
|
||||
defaultMessage: 'Select components',
|
||||
description: 'Problem bank component picker modal title.',
|
||||
},
|
||||
multipleComponentPickerModalBtn: {
|
||||
id: 'course-authoring.course-unit.modal.multiple-btn.text',
|
||||
defaultMessage: 'Add selected components',
|
||||
description: 'Problem bank component add button text.',
|
||||
},
|
||||
modalContainerTitle: {
|
||||
id: 'course-authoring.course-unit.modal.container.title',
|
||||
defaultMessage: 'Add {componentTitle} component',
|
||||
description: 'Modal title for adding components',
|
||||
},
|
||||
modalContainerCancelBtnText: {
|
||||
id: 'course-authoring.course-unit.modal.container.cancel.button.text',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Modal cancel button text.',
|
||||
},
|
||||
modalComponentSupportLabelFullySupported: {
|
||||
id: 'course-authoring.course-unit.modal.component.support.label.fully-supported',
|
||||
|
||||
78
src/course-unit/breadcrumbs/Breadcrumbs.jsx
Normal file
78
src/course-unit/breadcrumbs/Breadcrumbs.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown, Icon } from '@openedx/paragon';
|
||||
import {
|
||||
ArrowDropDown as ArrowDropDownIcon,
|
||||
ChevronRight as ChevronRightIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { createCorrectInternalRoute } from '../../utils';
|
||||
import { getCourseSectionVertical } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const Breadcrumbs = () => {
|
||||
const intl = useIntl();
|
||||
const { ancestorXblocks } = useSelector(getCourseSectionVertical);
|
||||
const [section, subsection] = ancestorXblocks ?? [];
|
||||
|
||||
return (
|
||||
<nav className="d-flex align-center mb-2.5">
|
||||
<ol className="p-0 m-0 d-flex align-center">
|
||||
<li className="d-flex">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="breadcrumbs-dropdown-section" variant="link" className="p-0 text-primary small">
|
||||
<span className="small text-gray-700">{section.title}</span>
|
||||
<Icon
|
||||
src={ArrowDropDownIcon}
|
||||
className="text-primary ml-1"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{section.children.map(({ url, displayName }) => (
|
||||
<Dropdown.Item
|
||||
key={url}
|
||||
href={createCorrectInternalRoute(url)}
|
||||
className="small"
|
||||
data-testid="breadcrumbs-section-dropdown-item"
|
||||
>
|
||||
{displayName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Icon
|
||||
src={ChevronRightIcon}
|
||||
size="md"
|
||||
className="text-primary mx-2"
|
||||
alt={intl.formatMessage(messages.altIconChevron)}
|
||||
/>
|
||||
</li>
|
||||
<li className="d-flex">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="breadcrumbs-dropdown-subsection" variant="link" className="p-0 text-primary">
|
||||
<span className="small text-gray-700">{subsection.title}</span>
|
||||
<Icon
|
||||
src={ArrowDropDownIcon}
|
||||
className="text-primary ml-1"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{subsection.children.map(({ url, displayName }) => (
|
||||
<Dropdown.Item
|
||||
key={url}
|
||||
href={createCorrectInternalRoute(url)}
|
||||
className="small"
|
||||
data-testid="breadcrumbs-subsection-dropdown-item"
|
||||
>
|
||||
{displayName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
||||
86
src/course-unit/breadcrumbs/Breadcrumbs.test.jsx
Normal file
86
src/course-unit/breadcrumbs/Breadcrumbs.test.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
|
||||
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
|
||||
import Breadcrumbs from './Breadcrumbs';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseId = '123';
|
||||
const breadcrumbsExpected = {
|
||||
section: {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
displayName: 'Example Week 1: Getting Started',
|
||||
},
|
||||
subsection: {
|
||||
displayName: 'Lesson 1 - Getting Started',
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = () => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<Breadcrumbs courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<Breadcrumbs />', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
it('render Breadcrumbs component correctly', async () => {
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument();
|
||||
expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('render Breadcrumbs\'s dropdown menus correctly', async () => {
|
||||
const { getByText, queryAllByTestId } = renderComponent();
|
||||
|
||||
expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument();
|
||||
expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument();
|
||||
expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(0);
|
||||
expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(0);
|
||||
|
||||
const button = getByText(breadcrumbsExpected.section.displayName);
|
||||
userEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(5);
|
||||
});
|
||||
|
||||
userEvent.click(getByText(breadcrumbsExpected.subsection.displayName));
|
||||
expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
initializeMocks, waitFor, act, render,
|
||||
} from '../../testUtils';
|
||||
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
|
||||
import { getApiWaffleFlagsUrl } from '../../data/api';
|
||||
import { fetchWaffleFlags } from '../../data/thunks';
|
||||
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
|
||||
import Breadcrumbs from './Breadcrumbs';
|
||||
|
||||
let axiosMock;
|
||||
let reduxStore;
|
||||
const courseId = '123';
|
||||
const parentUnitId = '456';
|
||||
const mockNavigate = jest.fn();
|
||||
const breadcrumbsExpected = {
|
||||
section: {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
displayName: 'Example Week 1: Getting Started',
|
||||
},
|
||||
subsection: {
|
||||
displayName: 'Lesson 1 - Getting Started',
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
const renderComponent = () => render(
|
||||
<Breadcrumbs courseId={courseId} parentUnitId={parentUnitId} />,
|
||||
);
|
||||
|
||||
describe('<Breadcrumbs />', () => {
|
||||
beforeEach(async () => {
|
||||
const mocks = initializeMocks();
|
||||
axiosMock = mocks.axiosMock;
|
||||
reduxStore = mocks.reduxStore;
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), reduxStore.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch);
|
||||
axiosMock
|
||||
.onGet(getApiWaffleFlagsUrl(courseId))
|
||||
.reply(200, { useNewCourseOutlinePage: true });
|
||||
await executeThunk(fetchWaffleFlags(courseId), reduxStore.dispatch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it('render Breadcrumbs component correctly', async () => {
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument();
|
||||
expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('render Breadcrumbs with many ancestors items correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
ancestor_xblocks: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
...courseSectionVerticalMock.ancestor_xblocks[0],
|
||||
display_name: 'Some module unit 1',
|
||||
},
|
||||
{
|
||||
...courseSectionVerticalMock.ancestor_xblocks[1],
|
||||
display_name: 'Some module unit 2',
|
||||
},
|
||||
],
|
||||
title: 'Some module',
|
||||
is_last: false,
|
||||
},
|
||||
...courseSectionVerticalMock.ancestor_xblocks,
|
||||
],
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch);
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Some module')).toBeInTheDocument();
|
||||
expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument();
|
||||
expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('render Breadcrumbs\'s dropdown menus correctly', async () => {
|
||||
const { getByText, queryAllByTestId } = renderComponent();
|
||||
|
||||
expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument();
|
||||
expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument();
|
||||
expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(0);
|
||||
expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(0);
|
||||
|
||||
const button = getByText(breadcrumbsExpected.section.displayName);
|
||||
userEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(queryAllByTestId('breadcrumbs-dropdown-item-level-0')).toHaveLength(5);
|
||||
});
|
||||
|
||||
userEvent.click(getByText(breadcrumbsExpected.subsection.displayName));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByTestId('breadcrumbs-dropdown-item-level-1')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates using the new course outline page when the waffle flag is enabled', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { ancestor_xblocks: [{ children: [{ display_name, url }] }] } = courseSectionVerticalMock;
|
||||
const { getByText, getByRole } = renderComponent();
|
||||
|
||||
await act(async () => {
|
||||
const dropdownBtn = getByText(breadcrumbsExpected.section.displayName);
|
||||
userEvent.click(dropdownBtn);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const dropdownItem = getByRole('link', { name: display_name });
|
||||
userEvent.click(dropdownItem);
|
||||
expect(dropdownItem).toHaveAttribute('href', url);
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to window.location.href when the waffle flag is disabled', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { ancestor_xblocks: [{ children: [{ display_name, url }] }] } = courseSectionVerticalMock;
|
||||
axiosMock
|
||||
.onGet(getApiWaffleFlagsUrl(courseId))
|
||||
.reply(200, { useNewCourseOutlinePage: false });
|
||||
await executeThunk(fetchWaffleFlags(courseId), reduxStore.dispatch);
|
||||
|
||||
const { getByText, getByRole } = renderComponent();
|
||||
|
||||
const dropdownBtn = getByText(breadcrumbsExpected.section.displayName);
|
||||
userEvent.click(dropdownBtn);
|
||||
|
||||
const dropdownItem = getByRole('link', { name: display_name });
|
||||
expect(dropdownItem).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${url}`);
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Dropdown, Icon } from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowDropDown as ArrowDropDownIcon,
|
||||
ChevronRight as ChevronRightIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { getWaffleFlags } from '../../data/selectors';
|
||||
import { getCourseSectionVertical } from '../data/selectors';
|
||||
import { adoptCourseSectionUrl } from '../utils';
|
||||
|
||||
const Breadcrumbs = ({ courseId, parentUnitId }: { courseId: string, parentUnitId: string }) => {
|
||||
const { ancestorXblocks = [] } = useSelector(getCourseSectionVertical);
|
||||
const waffleFlags = useSelector(getWaffleFlags);
|
||||
|
||||
const getPathToCourseOutlinePage = (url) => (waffleFlags.useNewCourseOutlinePage
|
||||
? url : `${getConfig().STUDIO_BASE_URL}${url}`);
|
||||
|
||||
const getPathToCourseUnitPage = (url) => (waffleFlags.useNewUnitPage
|
||||
? adoptCourseSectionUrl({ url, courseId, parentUnitId })
|
||||
: `${getConfig().STUDIO_BASE_URL}${url}`);
|
||||
|
||||
const getPathToCoursePage = (isOutlinePage, url) => (
|
||||
isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url)
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className="d-flex align-center mb-2.5">
|
||||
<ol className="p-0 m-0 d-flex align-center">
|
||||
{ancestorXblocks.map(({ children, title, isLast }, index) => (
|
||||
<li
|
||||
className="d-flex"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${title}-${index}`}
|
||||
>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="breadcrumbs-dropdown-section"
|
||||
variant="link"
|
||||
className="p-0 text-primary small"
|
||||
>
|
||||
<span className="small text-gray-700">
|
||||
{title}
|
||||
</span>
|
||||
<Icon
|
||||
src={ArrowDropDownIcon}
|
||||
className="text-primary ml-1"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{children.map(({ url, displayName }) => (
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
key={url}
|
||||
to={getPathToCoursePage(index < 2, url)}
|
||||
className="small"
|
||||
data-testid={`breadcrumbs-dropdown-item-level-${index}`}
|
||||
>
|
||||
{displayName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
{!isLast && (
|
||||
<Icon
|
||||
src={ChevronRightIcon}
|
||||
size="md"
|
||||
className="text-primary mx-2"
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as PasteNotificationAlert } from './paste-notification';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FILE_LIST_DEFAULT_VALUE } from '../constants';
|
||||
const FileList = ({ fileList }) => (
|
||||
<ul>
|
||||
{fileList.map((fileName) => (
|
||||
<li key={fileName}>{fileName}</li>
|
||||
<li>{fileName}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -101,7 +101,7 @@ const PastNotificationAlert = ({ staticFileNotices, courseId }) => {
|
||||
PastNotificationAlert.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
staticFileNotices:
|
||||
PropTypes.shape({
|
||||
PropTypes.objectOf({
|
||||
conflictingFiles: PropTypes.arrayOf(PropTypes.string),
|
||||
errorFiles: PropTypes.arrayOf(PropTypes.string),
|
||||
newFiles: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
* @returns {boolean|null} - The status of the alert. Returns `true` if the fileList has length,
|
||||
* `false` if it does not, and `null` if fileList is not defined.
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getAlertStatus = (fileList, alertKey, alertState) => (
|
||||
fileList?.length ? fileList && alertState[alertKey] : null);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user