Compare commits
171 Commits
zhancock/r
...
rijuma/cou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a5ae3939b | ||
|
|
7ea0bd175b | ||
|
|
dae1d63e23 | ||
|
|
6ab0deb7b7 | ||
|
|
e5f04d92b9 | ||
|
|
f39a50e7dc | ||
|
|
72724bcafb | ||
|
|
964abbe0c3 | ||
|
|
96d20e20e6 | ||
|
|
a56fd7d0e1 | ||
|
|
679caa61f3 | ||
|
|
420060967b | ||
|
|
91d3762513 | ||
|
|
2e3ed087d1 | ||
|
|
d76d4db097 | ||
|
|
04b314d157 | ||
|
|
1db4848d1a | ||
|
|
8f294781d2 | ||
|
|
d6908abb13 | ||
|
|
96d3d0da7e | ||
|
|
14cc32fcf6 | ||
|
|
0ac127e4c9 | ||
|
|
06e5fb5a44 | ||
|
|
2235737490 | ||
|
|
fca32ae872 | ||
|
|
ae04e5b366 | ||
|
|
db3f1b9cb0 | ||
|
|
ec360bc545 | ||
|
|
6c5220b4d7 | ||
|
|
f433118a8d | ||
|
|
e798331855 | ||
|
|
adb5796ff6 | ||
|
|
9958638a86 | ||
|
|
b8e844eba6 | ||
|
|
da633ffbd9 | ||
|
|
46889c2aba | ||
|
|
3cbbb0272b | ||
|
|
911c7658f5 | ||
|
|
b54d1e467e | ||
|
|
e34d18d727 | ||
|
|
6949e5708f | ||
|
|
eef6b1efe2 | ||
|
|
9dc45e192d | ||
|
|
bd9c97c269 | ||
|
|
c70fb138f0 | ||
|
|
8823cfaa0a | ||
|
|
7865fadec2 | ||
|
|
5be1620f1d | ||
|
|
d5a6a59d07 | ||
|
|
826f1382dd | ||
|
|
5e5fdeba44 | ||
|
|
01369eb00d | ||
|
|
4bb4bb7a88 | ||
|
|
1d154f46c1 | ||
|
|
420db8133f | ||
|
|
1ffc93dc6d | ||
|
|
346e15abd4 | ||
|
|
4726c23bc3 | ||
|
|
cbbb417894 | ||
|
|
c57f28ad40 | ||
|
|
310fb84517 | ||
|
|
623f6946e5 | ||
|
|
cf124877e8 | ||
|
|
0456ad9318 | ||
|
|
7be87b0f83 | ||
|
|
cbe5b28762 | ||
|
|
4a80532b8d | ||
|
|
e505f78cfb | ||
|
|
3811f5f9d5 | ||
|
|
8a20b908c7 | ||
|
|
8a6fa937ea | ||
|
|
dafdcad2b4 | ||
|
|
cd56ffaf9d | ||
|
|
c11cb85d78 | ||
|
|
b09bcbd3ae | ||
|
|
4a925f9c11 | ||
|
|
f5b6243c61 | ||
|
|
98c670afe7 | ||
|
|
038b05ba6c | ||
|
|
020e7fb42c | ||
|
|
ead98538b9 | ||
|
|
90ef6ace5c | ||
|
|
e0196f2a2a | ||
|
|
b7befcff7e | ||
|
|
642031bf87 | ||
|
|
f778f27647 | ||
|
|
b3bce8713c | ||
|
|
dacb30c73e | ||
|
|
81a4deeec0 | ||
|
|
9a1b05a1a4 | ||
|
|
b9e1fb0d2b | ||
|
|
ebd0f8816c | ||
|
|
d749429361 | ||
|
|
19b8df35ae | ||
|
|
e468d2087b | ||
|
|
42e0ac86d7 | ||
|
|
ea5cf37fd8 | ||
|
|
e4cdec7389 | ||
|
|
8aafc6b8bd | ||
|
|
913c8e4086 | ||
|
|
c221770213 | ||
|
|
4fe40c264f | ||
|
|
c20c7677a3 | ||
|
|
2ff8c3949e | ||
|
|
4a5c43d365 | ||
|
|
4da37f369b | ||
|
|
0effb32318 | ||
|
|
6813872dd3 | ||
|
|
e337a367d1 | ||
|
|
65343470e1 | ||
|
|
e69114a839 | ||
|
|
2d63a14c2e | ||
|
|
2d1f893a40 | ||
|
|
64f92deeb1 | ||
|
|
d47433ee83 | ||
|
|
6f1159617e | ||
|
|
8cc6b8cdde | ||
|
|
048488fb25 | ||
|
|
2a58ad2477 | ||
|
|
798c51b4e7 | ||
|
|
9a83d67d78 | ||
|
|
356b183c5c | ||
|
|
d64a4e448b | ||
|
|
860b3f9952 | ||
|
|
4418c5422f | ||
|
|
372c9de1db | ||
|
|
65adaf18d4 | ||
|
|
ed77465282 | ||
|
|
f5f6747ecb | ||
|
|
fbe16483ac | ||
|
|
e4a0105042 | ||
|
|
1d19ae0e7b | ||
|
|
b9d11982e3 | ||
|
|
73590f1ccd | ||
|
|
f8d35bf45d | ||
|
|
2038bad822 | ||
|
|
6c11947397 | ||
|
|
26565cd89c | ||
|
|
3a203e8351 | ||
|
|
500e4abcb9 | ||
|
|
d78851bb5b | ||
|
|
8c9a43d02b | ||
|
|
13c7c1de89 | ||
|
|
ba44b28cec | ||
|
|
79affe0629 | ||
|
|
d0ec7e3fb2 | ||
|
|
3f8b8077a9 | ||
|
|
290f17d76d | ||
|
|
64a1149550 | ||
|
|
e907ade40a | ||
|
|
68a7bf5527 | ||
|
|
db75ea28e4 | ||
|
|
ba4bdfe6af | ||
|
|
b63508db97 | ||
|
|
82b27e59cc | ||
|
|
ec8b5c5d6e | ||
|
|
dc1e9cd2e8 | ||
|
|
a681333a08 | ||
|
|
7cbbc720d1 | ||
|
|
863a838e6e | ||
|
|
5b046e828a | ||
|
|
a8f72c5e75 | ||
|
|
bb6c678904 | ||
|
|
71c2a31531 | ||
|
|
99a44dda37 | ||
|
|
5ae86465cc | ||
|
|
6e9c105eb9 | ||
|
|
26199fa954 | ||
|
|
3a542766d7 | ||
|
|
bbe03dc46f | ||
|
|
167d51b596 |
2
.env
2
.env
@@ -4,6 +4,7 @@
|
||||
NODE_ENV='production'
|
||||
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
APP_ID='learning'
|
||||
BASE_URL=''
|
||||
CONTACT_URL=''
|
||||
CREDENTIALS_BASE_URL=''
|
||||
@@ -47,3 +48,4 @@ TWITTER_HASHTAG=''
|
||||
TWITTER_URL=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
NODE_ENV='development'
|
||||
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
APP_ID='learning'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
@@ -49,3 +50,4 @@ SESSION_COOKIE_DOMAIN='localhost'
|
||||
CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
NODE_ENV='test'
|
||||
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
APP_ID='learning'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
@@ -46,3 +47,4 @@ TWITTER_HASHTAG='myedxjourney'
|
||||
TWITTER_URL='https://twitter.com/edXOnline'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
|
||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
26
.github/workflows/validate.yml
vendored
26
.github/workflows/validate.yml
vendored
@@ -10,15 +10,27 @@ jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version-file: '.nvmrc'
|
||||
- run: make validate.ci
|
||||
- name: Archive code coverage results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: code-coverage-report
|
||||
path: coverage/*.*
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download code coverage results
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: code-coverage-report
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
1
.husky/_/.gitignore
vendored
1
.husky/_/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/sh
|
||||
if [ -z "$husky_skip_init" ]; then
|
||||
debug () {
|
||||
if [ "$HUSKY_DEBUG" = "1" ]; then
|
||||
echo "husky (debug) - $1"
|
||||
fi
|
||||
}
|
||||
|
||||
readonly hook_name="$(basename "$0")"
|
||||
debug "starting $hook_name..."
|
||||
|
||||
if [ "$HUSKY" = "0" ]; then
|
||||
debug "HUSKY env variable is set to 0, skipping hook"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f ~/.huskyrc ]; then
|
||||
debug "sourcing ~/.huskyrc"
|
||||
. ~/.huskyrc
|
||||
fi
|
||||
|
||||
export readonly husky_skip_init=1
|
||||
sh -e "$0" "$@"
|
||||
exitCode="$?"
|
||||
|
||||
if [ $exitCode != 0 ]; then
|
||||
echo "husky - $hook_name hook exited with code $exitCode (error)"
|
||||
fi
|
||||
|
||||
exit $exitCode
|
||||
fi
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
2
Makefile
2
Makefile
@@ -55,8 +55,10 @@ validate:
|
||||
make validate-no-uncommitted-package-lock-changes
|
||||
npm run i18n_extract
|
||||
npm run lint -- --max-warnings 0
|
||||
npm run types
|
||||
npm run test
|
||||
npm run build
|
||||
npm run bundlewatch
|
||||
|
||||
.PHONY: validate.ci
|
||||
validate.ci:
|
||||
|
||||
66
README.rst
66
README.rst
@@ -21,25 +21,19 @@ Getting Started
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
The `devstack`_ is currently recommended as a development environment for your
|
||||
new MFE. If you start it with ``make dev.up.lms`` that should give you
|
||||
everything you need as a companion to this frontend.
|
||||
|
||||
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
|
||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
.. _Devstack: https://github.com/openedx/devstack
|
||||
`Tutor`_ is currently recommended as a development environment for the Learning
|
||||
MFE. Most likely, it already has this MFE configured; however, you'll need to
|
||||
make some changes in order to run it in development mode. You can refer
|
||||
to the `relevant tutor-mfe documentation`_ for details, or follow the quick
|
||||
guide below.
|
||||
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
|
||||
|
||||
To use this application, `devstack <https://github.com/openedx/devstack>`__ must be running and you must be logged into it.
|
||||
|
||||
- Visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
|
||||
|
||||
Cloning and Startup
|
||||
===================
|
||||
Cloning and Setup
|
||||
=================
|
||||
|
||||
1. Clone your new repo:
|
||||
|
||||
@@ -47,24 +41,62 @@ Cloning and Startup
|
||||
|
||||
git clone https://github.com/openedx/frontend-app-learning.git
|
||||
|
||||
2. Use node v18.x.
|
||||
2. Use node v20.x.
|
||||
|
||||
The current version of the micro-frontend build scripts supports node 18.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an ``.nvmrc`` file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
|
||||
3. Install npm dependencies:
|
||||
3. Stop the Tutor devstack, if it's running: ``tutor dev stop``
|
||||
|
||||
4. Next, we need to tell Tutor that we're going to be running this repo in
|
||||
development mode, and it should be excluded from the ``mfe`` container that
|
||||
otherwise runs every MFE. Run this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tutor mounts add /path/to/frontend-app-learning
|
||||
|
||||
5. Start Tutor in development mode. This command will start the LMS and Studio,
|
||||
and other required MFEs like ``authn`` and ``account``, but will not start
|
||||
the learning MFE, which we're going to run on the host instead of in a
|
||||
container managed by Tutor. Run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tutor dev start lms cms mfe
|
||||
|
||||
Startup
|
||||
=======
|
||||
|
||||
1. Install npm dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd frontend-app-learning && npm ci
|
||||
|
||||
4. Start the dev server:
|
||||
2. Start the dev server:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
npm start
|
||||
npm run dev
|
||||
|
||||
Then you can access the app at http://local.openedx.io:2000/learning/
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tutor dev stop
|
||||
tutor config save --set LMS_HOST=local.openedx.io --set CMS_HOST=studio.local.openedx.io
|
||||
tutor dev launch -I --skip-build
|
||||
tutor dev stop learning # We will run this MFE on the host
|
||||
|
||||
Local module development
|
||||
=========================
|
||||
|
||||
@@ -12,7 +12,8 @@ metadata:
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-aurora
|
||||
owner: group:committers-frontend-app-learning
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
|
||||
@@ -9,12 +9,12 @@ const config = createConfig('jest', {
|
||||
'src/i18n',
|
||||
'src/.*\\.exp\\..*',
|
||||
],
|
||||
// see https://github.com/axios/axios/issues/5026
|
||||
moduleNameMapper: {
|
||||
"^axios$": "axios/dist/axios.js",
|
||||
// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
|
||||
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
|
||||
'@src/(.*)': '<rootDir>/src/$1',
|
||||
// Explicit mapping to ensure Jest resolves the module correctly
|
||||
'@edx/frontend-lib-special-exams': '<rootDir>/node_modules/@edx/frontend-lib-special-exams',
|
||||
},
|
||||
testTimeout: 30000,
|
||||
globalSetup: "./global-setup.js",
|
||||
@@ -26,7 +26,7 @@ const config = createConfig('jest', {
|
||||
|
||||
config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
|
||||
// change this setting if need to see less details for each test
|
||||
// reportType: "summary" | "details",
|
||||
// reportType: "summary" | "details",
|
||||
// enable: true | false,
|
||||
afterEachTest: {
|
||||
enable: true,
|
||||
|
||||
10
openedx.yaml
10
openedx.yaml
@@ -1,10 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
|
||||
|
||||
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
|
||||
15476
package-lock.json
generated
15476
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@@ -11,14 +11,17 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"bundlewatch": "bundlewatch",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
||||
"prepare": "husky install",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"postinstall": "patch-package",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests",
|
||||
"test:watch": "fedx-scripts jest --watch --passWithNoTests",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -31,28 +34,32 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-header": "^5.0.2",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.2.2",
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/frontend-component-header": "^5.8.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.19.2",
|
||||
"@edx/frontend-lib-special-exams": "^3.1.3",
|
||||
"@edx/frontend-platform": "^7.1.2",
|
||||
"@edx/frontend-platform": "^8.0.0",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/react-unit-test-utils": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@edx/react-unit-test-utils": "3.0.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"@openedx/frontend-plugin-framework": "^1.1.2",
|
||||
"@openedx/frontend-build": "^14.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.1",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.3.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@reduxjs/toolkit": "1.8.1",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.22.2",
|
||||
"history": "5.3.0",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "2.5.1",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"joi": "^17.11.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "^7.1.3",
|
||||
"react": "17.0.2",
|
||||
@@ -62,35 +69,35 @@
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-share": "4.4.1",
|
||||
"redux": "4.1.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"redux": "4.2.1",
|
||||
"reselect": "4.1.8",
|
||||
"truncate-html": "1.0.4",
|
||||
"util": "0.12.5"
|
||||
"sass": "^1.79.3",
|
||||
"sass-loader": "^16.0.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"truncate-html": "1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@openedx/frontend-build": "13.1.4",
|
||||
"@pact-foundation/pact": "^11.0.2",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@pact-foundation/pact": "^13.0.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"es-check": "6.2.1",
|
||||
"eslint-import-resolver-webpack": "^0.13.8",
|
||||
"husky": "7.0.4",
|
||||
"jest": "^26.6.3",
|
||||
"jest-console-group-reporter": "^1.0.1",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"axios-mock-adapter": "2.1.0",
|
||||
"bundlewatch": "^0.4.0",
|
||||
"eslint-import-resolver-webpack": "^0.13.9",
|
||||
"jest": "^29.7.0",
|
||||
"jest-console-group-reporter": "^1.1.1",
|
||||
"jest-when": "^3.6.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"rosie": "2.1.1",
|
||||
"sass": "^1.72.0",
|
||||
"sass-loader": "^14.1.1",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"style-loader": "^3.3.4"
|
||||
"rosie": "2.1.1"
|
||||
},
|
||||
"bundlewatch": {
|
||||
"files": [
|
||||
{
|
||||
"path": "dist/*.js",
|
||||
"maxSize": "1400kB"
|
||||
}
|
||||
],
|
||||
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script src="https://www.edx.org/optimizelyjs/<%= htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
||||
<% } %>
|
||||
<% if (htmlWebpackPlugin.options.META_TAG_ROBOTS_CONTENT_ATTR) { %>
|
||||
<meta name="robots" content="<%= htmlWebpackPlugin.options.META_TAG_ROBOTS_CONTENT_ATTR %>">
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -13,18 +13,21 @@ export const DECODE_ROUTES = {
|
||||
'/course/:courseId/:sequenceId/:unitId',
|
||||
'/course/:courseId/:sequenceId',
|
||||
'/course/:courseId',
|
||||
'/preview/course/:courseId/:sequenceId/:unitId',
|
||||
'/preview/course/:courseId/:sequenceId',
|
||||
],
|
||||
REDIRECT_HOME: 'home/:courseId',
|
||||
REDIRECT_SURVEY: 'survey/:courseId',
|
||||
};
|
||||
} as const satisfies Readonly<{ [k: string]: string | readonly string[] }>;
|
||||
|
||||
export const ROUTES = {
|
||||
UNSUBSCRIBE: '/goal-unsubscribe/:token',
|
||||
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch',
|
||||
REDIRECT: '/redirect/*',
|
||||
DASHBOARD: 'dashboard',
|
||||
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
|
||||
CONSENT: 'consent',
|
||||
};
|
||||
} as const satisfies Readonly<{ [k: string]: string }>;
|
||||
|
||||
export const REDIRECT_MODES = {
|
||||
DASHBOARD_REDIRECT: 'dashboard-redirect',
|
||||
@@ -32,7 +35,7 @@ export const REDIRECT_MODES = {
|
||||
CONSENT_REDIRECT: 'consent-redirect',
|
||||
HOME_REDIRECT: 'home-redirect',
|
||||
SURVEY_REDIRECT: 'survey-redirect',
|
||||
};
|
||||
} as const satisfies Readonly<{ [k: string]: string }>;
|
||||
|
||||
export const VERIFIED_MODES = [
|
||||
'professional',
|
||||
@@ -43,9 +46,29 @@ export const VERIFIED_MODES = [
|
||||
'executive-education',
|
||||
'paid-executive-education',
|
||||
'paid-bootcamp',
|
||||
];
|
||||
] as const satisfies readonly string[];
|
||||
|
||||
export const AUDIT_MODES = [
|
||||
'audit',
|
||||
'honor',
|
||||
'unpaid-executive-education',
|
||||
'unpaid-bootcamp',
|
||||
] as const satisfies readonly string[];
|
||||
|
||||
// In sync with CourseMode.UPSELL_TO_VERIFIED_MODES
|
||||
// https://github.com/openedx/edx-platform/blob/master/common/djangoapps/course_modes/models.py#L231
|
||||
export const ALLOW_UPSELL_MODES = [
|
||||
'audit',
|
||||
'honor',
|
||||
] as const satisfies readonly string[];
|
||||
|
||||
export const WIDGETS = {
|
||||
DISCUSSIONS: 'DISCUSSIONS',
|
||||
NOTIFICATIONS: 'NOTIFICATIONS',
|
||||
};
|
||||
} as const satisfies Readonly<{ [k: string]: string }>;
|
||||
|
||||
export const LOADING = 'loading';
|
||||
export const LOADED = 'loaded';
|
||||
export const FAILED = 'failed';
|
||||
export const DENIED = 'denied';
|
||||
export type StatusValue = typeof LOADING | typeof LOADED | typeof FAILED | typeof DENIED;
|
||||
@@ -63,6 +63,7 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
|
||||
variant="tabs"
|
||||
activeKey={activeKey}
|
||||
onSelect={setFilter}
|
||||
aria-label={intl.formatMessage(messages.searchResultsFilterDescription)}
|
||||
>
|
||||
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
|
||||
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert, Button, Icon, Spinner,
|
||||
} from '@openedx/paragon';
|
||||
@@ -18,7 +18,8 @@ import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter';
|
||||
import { updateModel, useModel } from '../../generic/model-store';
|
||||
import { searchCourseContent } from '../data/thunks';
|
||||
|
||||
const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
||||
const CoursewareSearch = ({ ...sectionProps }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseId } = useParams();
|
||||
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,6 +30,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
||||
errors,
|
||||
total,
|
||||
} = useModel('contentSearchResults', courseId);
|
||||
const dialogRef = useRef();
|
||||
|
||||
useLockScroll();
|
||||
|
||||
@@ -44,7 +46,8 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
||||
searchKeyword: '',
|
||||
results: [],
|
||||
errors: undefined,
|
||||
loading: false,
|
||||
loading:
|
||||
false,
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -66,20 +69,46 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
||||
setQuery(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleSubmit(searchKeyword);
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (value) => {
|
||||
if (value === searchKeyword) { return; }
|
||||
if (!value) { clearSearch(); }
|
||||
};
|
||||
|
||||
const handleSearchCloseClick = () => {
|
||||
const close = () => {
|
||||
clearSearch();
|
||||
dispatch(setShowSearch(false));
|
||||
};
|
||||
|
||||
const handlePopState = () => close();
|
||||
|
||||
const handleBackdropClick = function (event) {
|
||||
if (event.target === dialogRef.current) {
|
||||
dialogRef.current.close();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// We need this to keep the dialog reference when unmounting.
|
||||
const dialog = dialogRef.current;
|
||||
|
||||
// Open the dialog as a modal on render to confine focus within it.
|
||||
dialogRef.current.showModal();
|
||||
|
||||
if (searchKeyword) {
|
||||
handleSubmit(searchKeyword); // In case it's opened with a search link, we run the search.
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
window.addEventListener('popstate', handlePopState, { signal });
|
||||
dialog.addEventListener('click', handleBackdropClick, { signal });
|
||||
|
||||
return () => controller.abort(); // Removes event listeners.
|
||||
}, []);
|
||||
|
||||
const handleSearchClose = () => close();
|
||||
|
||||
let status = 'idle';
|
||||
if (loading) {
|
||||
status = 'loading';
|
||||
@@ -90,59 +119,64 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-section" {...sectionProps}>
|
||||
<div className="courseware-search__close">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="p-1"
|
||||
aria-label={intl.formatMessage(messages.searchCloseAction)}
|
||||
onClick={handleSearchCloseClick}
|
||||
data-testid="courseware-search-close-button"
|
||||
><Icon src={Close} />
|
||||
</Button>
|
||||
</div>
|
||||
<dialog ref={dialogRef} className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-dialog" onClose={handleSearchClose} {...sectionProps}>
|
||||
<div className="courseware-search__outer-content">
|
||||
<div className="courseware-search__content">
|
||||
<h1 className="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1>
|
||||
<CoursewareSearchForm
|
||||
searchTerm={searchKeyword}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleOnChange}
|
||||
placeholder={intl.formatMessage(messages.searchBarPlaceholderText)}
|
||||
/>
|
||||
{status === 'loading' ? (
|
||||
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
||||
<Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} />
|
||||
<div className="courseware-search__content" data-testid="courseware-search-content">
|
||||
<div className="courseware-search__form">
|
||||
<h1 className="h2">{formatMessage(messages.searchModuleTitle)}</h1>
|
||||
<CoursewareSearchForm
|
||||
searchTerm={searchKeyword}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleOnChange}
|
||||
placeholder={formatMessage(messages.searchBarPlaceholderText)}
|
||||
/>
|
||||
<div className="courseware-search__close">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="p-1"
|
||||
aria-label={formatMessage(messages.searchCloseAction)}
|
||||
onClick={() => dialogRef.current.close()}
|
||||
data-testid="courseware-search-close-button"
|
||||
><Icon src={Close} />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{status === 'error' && (
|
||||
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
||||
{intl.formatMessage(messages.searchResultsError)}
|
||||
</Alert>
|
||||
)}
|
||||
{status === 'results' ? (
|
||||
<>
|
||||
{total > 0 ? (
|
||||
<div
|
||||
className="courseware-search__results-summary"
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
aria-atomic="true"
|
||||
data-testid="courseware-search-summary"
|
||||
>{intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
||||
</div>
|
||||
) : null}
|
||||
<CoursewareSearchResultsFilterContainer />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
key={status}
|
||||
className="courseware-search__results"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
aria-busy={status === 'loading'}
|
||||
data-testid="courseware-search-results"
|
||||
role={status === 'results' ? 'alert' : 'none'}
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
||||
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
|
||||
</div>
|
||||
) : null}
|
||||
{status === 'error' && (
|
||||
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
||||
{formatMessage(messages.searchResultsError)}
|
||||
</Alert>
|
||||
)}
|
||||
{status === 'results' ? (
|
||||
<>
|
||||
{total > 0 ? (
|
||||
<div
|
||||
className="courseware-search__results-summary"
|
||||
data-testid="courseware-search-summary"
|
||||
>{formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
||||
</div>
|
||||
) : null}
|
||||
<CoursewareSearchResultsFilterContainer />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
CoursewareSearch.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearch);
|
||||
export default CoursewareSearch;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
within,
|
||||
} from '../../setupTest';
|
||||
import { CoursewareSearch } from './index';
|
||||
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks';
|
||||
@@ -19,6 +20,7 @@ import { updateModel, useModel } from '../../generic/model-store';
|
||||
|
||||
jest.mock('./hooks');
|
||||
jest.mock('../../generic/model-store', () => ({
|
||||
...jest.requireActual('../../generic/model-store'),
|
||||
updateModel: jest.fn(),
|
||||
useModel: jest.fn(),
|
||||
}));
|
||||
@@ -56,7 +58,7 @@ const defaultProps = {
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const coursewareSearch = {
|
||||
const defaultSearchParams = {
|
||||
query: '',
|
||||
filter: '',
|
||||
setQuery: jest.fn(),
|
||||
@@ -96,14 +98,20 @@ const mockModels = ((props = defaultProps) => {
|
||||
});
|
||||
});
|
||||
|
||||
const mockSearchParams = ((props = coursewareSearch) => {
|
||||
const mockSearchParams = ((params) => {
|
||||
const props = { ...defaultSearchParams, ...params };
|
||||
useCoursewareSearchParams.mockReturnValue(props);
|
||||
});
|
||||
|
||||
describe('CoursewareSearch', () => {
|
||||
beforeAll(initializeMockApp);
|
||||
beforeAll(() => initializeMockApp());
|
||||
|
||||
beforeEach(() => {
|
||||
mockModels();
|
||||
mockSearchParams();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -113,27 +121,22 @@ describe('CoursewareSearch', () => {
|
||||
});
|
||||
|
||||
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
|
||||
mockModels();
|
||||
mockSearchParams();
|
||||
renderComponent();
|
||||
|
||||
expect(useElementBoundingBox).toBeCalledTimes(1);
|
||||
expect(useLockScroll).toBeCalledTimes(1);
|
||||
expect(useElementBoundingBox).toHaveBeenCalledTimes(1);
|
||||
expect(useLockScroll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
|
||||
mockModels();
|
||||
mockSearchParams();
|
||||
renderComponent();
|
||||
|
||||
const section = screen.getByTestId('courseware-search-section');
|
||||
const section = screen.getByTestId('courseware-search-dialog');
|
||||
expect(section.style.getPropertyValue('--modal-top-position')).toBe(`${tabsTopPosition}px`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clicking on the "Close" button', () => {
|
||||
it('should dispatch setShowSearch(false)', async () => {
|
||||
mockModels();
|
||||
it('should close the dialog', async () => {
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -141,7 +144,8 @@ describe('CoursewareSearch', () => {
|
||||
fireEvent.click(close);
|
||||
});
|
||||
|
||||
expect(setShowSearch).toBeCalledWith(false);
|
||||
expect(HTMLDialogElement.prototype.close).toHaveBeenCalled();
|
||||
expect(setShowSearch).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,29 +153,24 @@ describe('CoursewareSearch', () => {
|
||||
it('should use "--modal-top-position: 0" if nce element is not present', () => {
|
||||
useElementBoundingBox.mockImplementation(() => undefined);
|
||||
|
||||
mockModels();
|
||||
mockSearchParams();
|
||||
renderComponent();
|
||||
|
||||
const section = screen.getByTestId('courseware-search-section');
|
||||
const section = screen.getByTestId('courseware-search-dialog');
|
||||
expect(section.style.getPropertyValue('--modal-top-position')).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passing extra props', () => {
|
||||
it('should pass on extra props to section element', () => {
|
||||
mockModels();
|
||||
mockSearchParams();
|
||||
renderComponent({ foo: 'bar' });
|
||||
|
||||
const section = screen.getByTestId('courseware-search-section');
|
||||
const section = screen.getByTestId('courseware-search-dialog');
|
||||
expect(section).toHaveAttribute('foo', 'bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting an empty search', () => {
|
||||
it('should clear the search by dispatch updateModel', async () => {
|
||||
mockModels();
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -203,7 +202,6 @@ describe('CoursewareSearch', () => {
|
||||
});
|
||||
|
||||
it('should call searchCourseContent', async () => {
|
||||
mockModels();
|
||||
renderComponent();
|
||||
|
||||
const searchKeyword = 'course';
|
||||
@@ -246,19 +244,25 @@ describe('CoursewareSearch', () => {
|
||||
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show a summary for the results', () => {
|
||||
it('should show a wrapper div with proper aria attributes', () => {
|
||||
mockModels({
|
||||
searchKeyword: 'fubar',
|
||||
total: 1,
|
||||
});
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
|
||||
const results = screen.queryByTestId('courseware-search-results');
|
||||
|
||||
expect(results).toHaveAttribute('aria-live', 'assertive');
|
||||
expect(results).toHaveAttribute('aria-atomic', 'true');
|
||||
expect(results).toHaveAttribute('role', 'alert');
|
||||
expect(within(results).queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clearing the search input', () => {
|
||||
it('should clear the search by dispatch updateModel', async () => {
|
||||
mockSearchParams({ query: 'fubar' });
|
||||
mockModels({
|
||||
searchKeyword: 'fubar',
|
||||
total: 2,
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchField } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const CoursewareSearchForm = ({
|
||||
intl,
|
||||
searchTerm,
|
||||
onSubmit,
|
||||
onChange,
|
||||
placeholder,
|
||||
}) => (
|
||||
<SearchField.Advanced
|
||||
value={searchTerm}
|
||||
onSubmit={onSubmit}
|
||||
onChange={onChange}
|
||||
submitButtonLocation="external"
|
||||
className="courseware-search-form"
|
||||
screenReaderText={{
|
||||
label: intl.formatMessage(messages.searchSubmitLabel),
|
||||
clearButton: intl.formatMessage(messages.searchClearAction),
|
||||
submitButton: null, // Remove the sr-only label in the button.
|
||||
}}
|
||||
>
|
||||
<div className="pgn__searchfield_wrapper" data-testid="courseware-search-form">
|
||||
<SearchField.Label />
|
||||
<SearchField.Input placeholder={placeholder} autoFocus />
|
||||
<SearchField.ClearButton />
|
||||
</div>
|
||||
<SearchField.SubmitButton
|
||||
buttonText={intl.formatMessage(messages.searchSubmitLabel)}
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<SearchField.Advanced
|
||||
value={searchTerm}
|
||||
onSubmit={onSubmit}
|
||||
onChange={onChange}
|
||||
submitButtonLocation="external"
|
||||
data-testid="courseware-search-form-submit"
|
||||
/>
|
||||
</SearchField.Advanced>
|
||||
);
|
||||
className="courseware-search-form"
|
||||
screenReaderText={{
|
||||
label: formatMessage(messages.searchSubmitLabel),
|
||||
clearButton: formatMessage(messages.searchClearAction),
|
||||
submitButton: null, // Remove the sr-only label in the button.
|
||||
}}
|
||||
>
|
||||
<div className="pgn__searchfield_wrapper" data-testid="courseware-search-form">
|
||||
<SearchField.Label />
|
||||
<SearchField.Input placeholder={placeholder} autoFocus />
|
||||
<SearchField.ClearButton />
|
||||
</div>
|
||||
<SearchField.SubmitButton
|
||||
buttonText={formatMessage(messages.searchSubmitLabel)}
|
||||
submitButtonLocation="external"
|
||||
data-testid="courseware-search-form-submit"
|
||||
/>
|
||||
</SearchField.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
CoursewareSearchForm.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
searchTerm: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
@@ -51,4 +52,4 @@ CoursewareSearchForm.defaultProps = {
|
||||
placeholder: undefined,
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearchForm);
|
||||
export default CoursewareSearchForm;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Folder, TextFields, VideoCamera, Article,
|
||||
} from '@openedx/paragon/icons';
|
||||
@@ -6,6 +6,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||
import messages from './messages';
|
||||
|
||||
const iconTypeMapping = {
|
||||
text: TextFields,
|
||||
@@ -21,6 +22,8 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
||||
return <CoursewareSearchEmpty />;
|
||||
}
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const baseUrl = `${getConfig().LMS_BASE_URL}`;
|
||||
|
||||
return (
|
||||
@@ -42,24 +45,30 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
||||
rel: 'nofollow',
|
||||
} : { href: `${baseUrl}${url}` };
|
||||
|
||||
const ariaSeaparator = formatMessage(messages.searchResultsBreadcrumbSeparator);
|
||||
const ariaLocation = location?.length ? formatMessage(messages.searchResultsBreadcrumb, { path: location.join(ariaSeaparator) }) : '';
|
||||
|
||||
return (
|
||||
<a key={id} className="courseware-search-results__item" {...linkProps}>
|
||||
<div className="courseware-search-results__icon"><Icon src={icon} /></div>
|
||||
<div className="courseware-search-results__info">
|
||||
<div className="courseware-search-results__title">
|
||||
<span>{title}</span>
|
||||
{contentHits ? (<em>{contentHits}</em>) : null }
|
||||
<h3>{title}</h3>
|
||||
{contentHits ? (<em aria-hidden="true">{contentHits}</em>) : null }
|
||||
</div>
|
||||
{location?.length ? (
|
||||
<ul className="courseware-search-results__breadcrumbs">
|
||||
{
|
||||
|
||||
<div aria-label={ariaLocation}>
|
||||
{location?.length ? (
|
||||
<ul className="courseware-search-results__breadcrumbs" aria-hidden="true">
|
||||
{
|
||||
// This ignore is necessary because the breadcrumb texts might have duplicates.
|
||||
// The breadcrumbs are not expected to change.
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
location.map((breadcrumb, i) => (<li key={`${i}:${breadcrumb}`}><div>{breadcrumb}</div></li>))
|
||||
}
|
||||
</ul>
|
||||
) : null}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -38,24 +38,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Demo Course Overview
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -91,32 +96,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Passing a Course
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
1
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -152,29 +164,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Passing a Course
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -210,29 +227,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Text Input
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Text input."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Text input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Text input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -268,29 +290,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Pointing on a Picture
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Pointing on a Picture."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Pointing on a Picture
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Pointing on a Picture
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -326,29 +353,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Getting Answers
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Getting Answers."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Getting Answers
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Getting Answers
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -384,32 +416,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Welcome!
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
30
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -445,29 +484,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Multiple Choice Questions
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Multiple Choice Questions."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Multiple Choice Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Multiple Choice Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -503,29 +547,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Numerical Input
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Numerical Input."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Numerical Input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Numerical Input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -561,32 +610,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Connecting a Circuit and a Circuit Diagram
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Video Presentation Styles."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Video Presentation Styles
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Video Presentation Styles
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -622,29 +678,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
CAPA
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 2: Get Interactive, then Homework - Labs and Demos, then Code Grader."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 2: Get Interactive
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Labs and Demos
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Code Grader
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 2: Get Interactive
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Labs and Demos
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Code Grader
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -680,29 +741,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Interactive Questions
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Interactive Questions."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Interactive Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Interactive Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -738,32 +804,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Blank HTML Page
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
6
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -799,32 +872,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Discussion Forums
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
5
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Discussion Forums."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Discussion Forums
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Discussion Forums
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -860,32 +940,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Overall Grade
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
7
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Overall Grade Performance."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Overall Grade Performance
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Overall Grade Performance
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -921,32 +1008,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Blank HTML Page
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Homework - Find Your Study Buddy."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -982,32 +1076,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Find Your Study Buddy
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Homework - Find Your Study Buddy, then Homework - Find Your Study Buddy."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1043,32 +1144,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
Be Social
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
4
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Be Social."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Be Social
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Be Social
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1104,32 +1212,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
EdX Exams
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
4
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then EdX Exams."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
EdX Exams
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
EdX Exams
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1165,32 +1280,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
When Are Your Exams?
|
||||
</span>
|
||||
<em>
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
2
|
||||
</em>
|
||||
</div>
|
||||
<ul
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then When Are Your Exams? ."
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
When Are Your Exams?
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
When Are Your Exams?
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
@@ -1228,10 +1350,13 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<span>
|
||||
<h3>
|
||||
External Course Link Test
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label=""
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
{
|
||||
"filters": [
|
||||
{
|
||||
"count": 7,
|
||||
"key": "capa",
|
||||
"label": "CAPA",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"count": 2,
|
||||
"key": "sequence",
|
||||
"label": "Sequence",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"count": 9,
|
||||
"key": "text",
|
||||
"label": "Text",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"count": 1,
|
||||
"key": "unknown",
|
||||
"label": "Unknown",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"count": 2,
|
||||
"key": "video",
|
||||
"label": "Video",
|
||||
@@ -31,11 +31,11 @@ Object {
|
||||
],
|
||||
"maxScore": 3.4545178,
|
||||
"ms": 5,
|
||||
"results": Array [
|
||||
Object {
|
||||
"results": [
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
],
|
||||
@@ -44,10 +44,10 @@ Object {
|
||||
"type": "sequence",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Passing a Course",
|
||||
@@ -57,10 +57,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Passing a Course",
|
||||
@@ -70,10 +70,10 @@ Object {
|
||||
"type": "sequence",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Text input",
|
||||
@@ -83,10 +83,10 @@ Object {
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Pointing on a Picture",
|
||||
@@ -96,10 +96,10 @@ Object {
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Getting Answers",
|
||||
@@ -109,10 +109,10 @@ Object {
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
"Introduction: Video and Sequences",
|
||||
@@ -122,10 +122,10 @@ Object {
|
||||
"type": "video",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Multiple Choice Questions",
|
||||
@@ -135,10 +135,10 @@ Object {
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Numerical Input",
|
||||
@@ -148,10 +148,10 @@ Object {
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"Video Presentation Styles",
|
||||
@@ -161,10 +161,10 @@ Object {
|
||||
"type": "video",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 2: Get Interactive",
|
||||
"Homework - Labs and Demos",
|
||||
"Code Grader",
|
||||
@@ -174,10 +174,10 @@ Object {
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"Interactive Questions",
|
||||
@@ -187,10 +187,10 @@ Object {
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
"Introduction: Video and Sequences",
|
||||
@@ -200,10 +200,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Discussion Forums",
|
||||
@@ -213,10 +213,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Overall Grade Performance",
|
||||
@@ -226,10 +226,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Homework - Find Your Study Buddy",
|
||||
@@ -239,10 +239,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Homework - Find Your Study Buddy",
|
||||
"Homework - Find Your Study Buddy",
|
||||
@@ -252,10 +252,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Be Social",
|
||||
@@ -265,10 +265,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"EdX Exams",
|
||||
@@ -278,10 +278,10 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||
"location": Array [
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"When Are Your Exams? ",
|
||||
@@ -291,7 +291,7 @@ Object {
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "random-element-id",
|
||||
"location": null,
|
||||
|
||||
@@ -5,13 +5,25 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
border-top: 1px solid $light-300;
|
||||
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
|
||||
|
||||
&__form {
|
||||
position: relative;
|
||||
|
||||
.h2 {
|
||||
margin-right: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
position: absolute !important; // For some reason it gets overridden
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -89,6 +101,11 @@
|
||||
font-size: 0.875rem;
|
||||
color: $black;
|
||||
|
||||
> h3 {
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
@@ -152,9 +169,16 @@
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, 'md')) {
|
||||
.courseware-search__content {
|
||||
padding-top: 8rem;
|
||||
.courseware-search {
|
||||
&__close {
|
||||
right: -2.5rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding-top: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body._search-no-scroll {
|
||||
|
||||
@@ -56,7 +56,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.',
|
||||
description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.',
|
||||
},
|
||||
|
||||
searchResultsFilterDescription: {
|
||||
id: 'learn.coursewareSearch.searchResultsFilterDescription',
|
||||
defaultMessage: 'Search result filters',
|
||||
description: 'Screen Reader text to describe the filter options.',
|
||||
},
|
||||
searchResultsBreadcrumb: {
|
||||
id: 'learn.coursewareSearch.searchResultsBreadcrumb',
|
||||
defaultMessage: 'Location: {path}.',
|
||||
description: 'Screen Reader text to describe the search result breadcrumbs.',
|
||||
},
|
||||
searchResultsBreadcrumbSeparator: {
|
||||
id: 'learn.searchResultsBreadcrumbSeparator',
|
||||
defaultMessage: ', then ',
|
||||
description: 'Screen Reader text to connect breadcrumb sections. i.e.: "Introduction, then Register, then Something else.',
|
||||
},
|
||||
// These are translations for labeling the filters
|
||||
'filter:all': {
|
||||
id: 'learn.coursewareSearch.filter:all',
|
||||
@@ -1,8 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
||||
Object {
|
||||
"courseHome": Object {
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"proctoringPanelStatus": "loading",
|
||||
@@ -12,13 +12,13 @@ Object {
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": Object {
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": Object {},
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": Object {},
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
@@ -26,12 +26,12 @@ Object {
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": Object {
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
@@ -49,33 +49,33 @@ Object {
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": Array [
|
||||
Object {
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
@@ -84,7 +84,7 @@ Object {
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": Object {
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
@@ -94,10 +94,10 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"dates": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"courseDateBlocks": Array [
|
||||
Object {
|
||||
"dates": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"courseDateBlocks": [
|
||||
{
|
||||
"date": "2020-05-01T17:59:41Z",
|
||||
"dateType": "course-start-date",
|
||||
"description": "",
|
||||
@@ -106,7 +106,7 @@ Object {
|
||||
"link": "",
|
||||
"title": "Course Starts",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-04T02:59:40.942669Z",
|
||||
@@ -116,7 +116,7 @@ Object {
|
||||
"learnerHasAccess": true,
|
||||
"title": "Multi Badges Completed",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-05T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -125,7 +125,7 @@ Object {
|
||||
"learnerHasAccess": true,
|
||||
"title": "Multi Badges Past Due",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-27T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -135,7 +135,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Past Due 1",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-27T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -145,7 +145,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Past Due 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-28T08:59:40.942669Z",
|
||||
@@ -156,7 +156,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "One Completed/Due 1",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-28T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -166,7 +166,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "One Completed/Due 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-29T08:59:40.942669Z",
|
||||
@@ -177,7 +177,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Completed 1",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-29T08:59:40.942669Z",
|
||||
@@ -188,7 +188,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Completed 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"date": "2020-06-16T17:59:40.942669Z",
|
||||
"dateType": "verified-upgrade-deadline",
|
||||
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
|
||||
@@ -197,7 +197,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "Upgrade to Verified Certificate",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -207,7 +207,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "One Verified 1",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -217,7 +217,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "One Verified 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -227,7 +227,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "ORA Verified 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-18T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -237,7 +237,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Verified 1",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-18T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -247,7 +247,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Verified 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-19T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -255,7 +255,7 @@ Object {
|
||||
"learnerHasAccess": true,
|
||||
"title": "One Unreleased 1",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-19T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -265,7 +265,7 @@ Object {
|
||||
"link": "https://example.com/",
|
||||
"title": "One Unreleased 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-20T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -274,7 +274,7 @@ Object {
|
||||
"learnerHasAccess": true,
|
||||
"title": "Both Unreleased 1",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-20T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
@@ -283,7 +283,7 @@ Object {
|
||||
"learnerHasAccess": true,
|
||||
"title": "Both Unreleased 2",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"date": "2030-08-23T00:00:00Z",
|
||||
"dateType": "course-end-date",
|
||||
"description": "",
|
||||
@@ -292,7 +292,7 @@ Object {
|
||||
"link": "",
|
||||
"title": "Course Ends",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"date": "2030-09-01T00:00:00Z",
|
||||
"dateType": "verification-deadline-date",
|
||||
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
|
||||
@@ -302,7 +302,7 @@ Object {
|
||||
"title": "Verification Deadline",
|
||||
},
|
||||
],
|
||||
"datesBannerInfo": Object {
|
||||
"datesBannerInfo": {
|
||||
"contentTypeGatingEnabled": false,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
@@ -314,16 +314,16 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": Object {},
|
||||
"recommendations": Object {
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": Object {
|
||||
"specialExams": {
|
||||
"activeAttempt": null,
|
||||
"allowProctoringOptOut": false,
|
||||
"apiErrorMsg": "",
|
||||
"exam": Object {
|
||||
"attempt": Object {
|
||||
"exam": {
|
||||
"attempt": {
|
||||
"attempt_code": "",
|
||||
"attempt_id": null,
|
||||
"attempt_status": "",
|
||||
@@ -351,27 +351,27 @@ Object {
|
||||
"is_active": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored": false,
|
||||
"prerequisite_status": Object {
|
||||
"prerequisite_status": {
|
||||
"are_prerequisites_satisifed": true,
|
||||
"declined_prerequisites": Array [],
|
||||
"failed_prerequisites": Array [],
|
||||
"pending_prerequisites": Array [],
|
||||
"satisfied_prerequisites": Array [],
|
||||
"declined_prerequisites": [],
|
||||
"failed_prerequisites": [],
|
||||
"pending_prerequisites": [],
|
||||
"satisfied_prerequisites": [],
|
||||
},
|
||||
"time_limit_mins": null,
|
||||
"type": "",
|
||||
},
|
||||
"examAccessToken": Object {
|
||||
"examAccessToken": {
|
||||
"exam_access_token": "",
|
||||
"exam_access_token_expiration": "",
|
||||
},
|
||||
"isLoading": true,
|
||||
"proctoringSettings": Object {
|
||||
"exam_proctoring_backend": Object {
|
||||
"proctoringSettings": {
|
||||
"exam_proctoring_backend": {
|
||||
"download_url": "",
|
||||
"instructions": Array [],
|
||||
"instructions": [],
|
||||
"name": "",
|
||||
"rules": Object {},
|
||||
"rules": {},
|
||||
},
|
||||
"integration_specific_email": "",
|
||||
"learner_notification_from_email": "",
|
||||
@@ -382,7 +382,7 @@ Object {
|
||||
},
|
||||
"timeIsOver": false,
|
||||
},
|
||||
"tours": Object {
|
||||
"tours": {
|
||||
"showCoursewareTour": false,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
@@ -393,8 +393,8 @@ Object {
|
||||
`;
|
||||
|
||||
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
||||
Object {
|
||||
"courseHome": Object {
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"proctoringPanelStatus": "loading",
|
||||
@@ -404,13 +404,13 @@ Object {
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": Object {
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": Object {},
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": Object {},
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
@@ -418,12 +418,12 @@ Object {
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": Object {
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
@@ -441,33 +441,33 @@ Object {
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": Array [
|
||||
Object {
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
@@ -476,7 +476,7 @@ Object {
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": Object {
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
@@ -486,41 +486,41 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"outline": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"outline": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"accessExpiration": null,
|
||||
"canShowUpgradeSock": false,
|
||||
"certData": Object {
|
||||
"certData": {
|
||||
"certStatus": null,
|
||||
"certWebViewUrl": null,
|
||||
"certificateAvailableDate": null,
|
||||
},
|
||||
"courseBlocks": Object {
|
||||
"courses": Object {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
|
||||
"courseBlocks": {
|
||||
"courses": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": {
|
||||
"hasScheduledContent": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"sectionIds": Array [
|
||||
"sectionIds": [
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
],
|
||||
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
|
||||
},
|
||||
},
|
||||
"sections": Object {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
|
||||
"sections": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": {
|
||||
"complete": false,
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"hideFromTOC": undefined,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
"resumeBlock": false,
|
||||
"sequenceIds": Array [
|
||||
"sequenceIds": [
|
||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||
],
|
||||
"title": "Title of Section",
|
||||
},
|
||||
},
|
||||
"sequences": Object {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object {
|
||||
"sequences": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": {
|
||||
"complete": false,
|
||||
"description": null,
|
||||
"due": null,
|
||||
@@ -536,30 +536,30 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"courseGoals": Object {
|
||||
"courseGoals": {
|
||||
"daysPerWeek": null,
|
||||
"goalOptions": Array [],
|
||||
"goalOptions": [],
|
||||
"selectedGoal": null,
|
||||
"subscribedToReminders": null,
|
||||
"weeklyLearningGoalEnabled": false,
|
||||
},
|
||||
"courseTools": Array [
|
||||
Object {
|
||||
"courseTools": [
|
||||
{
|
||||
"analyticsId": "edx.bookmarks",
|
||||
"title": "Bookmarks",
|
||||
"url": "https://example.com/bookmarks",
|
||||
},
|
||||
],
|
||||
"datesBannerInfo": Object {
|
||||
"datesBannerInfo": {
|
||||
"contentTypeGatingEnabled": false,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
},
|
||||
"datesWidget": Object {
|
||||
"courseDateBlocks": Array [],
|
||||
"datesWidget": {
|
||||
"courseDateBlocks": [],
|
||||
},
|
||||
"enableProctoredExams": undefined,
|
||||
"enrollAlert": Object {
|
||||
"enrollAlert": {
|
||||
"canEnroll": true,
|
||||
"extraText": "Contact the administrator.",
|
||||
},
|
||||
@@ -569,13 +569,13 @@ Object {
|
||||
"hasScheduledContent": null,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"offer": null,
|
||||
"resumeCourse": Object {
|
||||
"resumeCourse": {
|
||||
"hasVisitedCourse": false,
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||
},
|
||||
"timeOffsetMillis": 0,
|
||||
"userHasPassingGrade": undefined,
|
||||
"verifiedMode": Object {
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": "2050-01-01T12:00:00",
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
@@ -587,16 +587,16 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": Object {},
|
||||
"recommendations": Object {
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": Object {
|
||||
"specialExams": {
|
||||
"activeAttempt": null,
|
||||
"allowProctoringOptOut": false,
|
||||
"apiErrorMsg": "",
|
||||
"exam": Object {
|
||||
"attempt": Object {
|
||||
"exam": {
|
||||
"attempt": {
|
||||
"attempt_code": "",
|
||||
"attempt_id": null,
|
||||
"attempt_status": "",
|
||||
@@ -624,27 +624,27 @@ Object {
|
||||
"is_active": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored": false,
|
||||
"prerequisite_status": Object {
|
||||
"prerequisite_status": {
|
||||
"are_prerequisites_satisifed": true,
|
||||
"declined_prerequisites": Array [],
|
||||
"failed_prerequisites": Array [],
|
||||
"pending_prerequisites": Array [],
|
||||
"satisfied_prerequisites": Array [],
|
||||
"declined_prerequisites": [],
|
||||
"failed_prerequisites": [],
|
||||
"pending_prerequisites": [],
|
||||
"satisfied_prerequisites": [],
|
||||
},
|
||||
"time_limit_mins": null,
|
||||
"type": "",
|
||||
},
|
||||
"examAccessToken": Object {
|
||||
"examAccessToken": {
|
||||
"exam_access_token": "",
|
||||
"exam_access_token_expiration": "",
|
||||
},
|
||||
"isLoading": true,
|
||||
"proctoringSettings": Object {
|
||||
"exam_proctoring_backend": Object {
|
||||
"proctoringSettings": {
|
||||
"exam_proctoring_backend": {
|
||||
"download_url": "",
|
||||
"instructions": Array [],
|
||||
"instructions": [],
|
||||
"name": "",
|
||||
"rules": Object {},
|
||||
"rules": {},
|
||||
},
|
||||
"integration_specific_email": "",
|
||||
"learner_notification_from_email": "",
|
||||
@@ -655,7 +655,7 @@ Object {
|
||||
},
|
||||
"timeIsOver": false,
|
||||
},
|
||||
"tours": Object {
|
||||
"tours": {
|
||||
"showCoursewareTour": false,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
@@ -666,8 +666,8 @@ Object {
|
||||
`;
|
||||
|
||||
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
||||
Object {
|
||||
"courseHome": Object {
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"proctoringPanelStatus": "loading",
|
||||
@@ -677,13 +677,13 @@ Object {
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": Object {
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": Object {},
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": Object {},
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
@@ -691,12 +691,12 @@ Object {
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": Object {
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
@@ -714,33 +714,33 @@ Object {
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": Array [
|
||||
Object {
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
@@ -749,7 +749,7 @@ Object {
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": Object {
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
@@ -759,16 +759,16 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"progress": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"progress": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"accessExpiration": null,
|
||||
"certificateData": Object {},
|
||||
"completionSummary": Object {
|
||||
"certificateData": {},
|
||||
"completionSummary": {
|
||||
"completeCount": 1,
|
||||
"incompleteCount": 1,
|
||||
"lockedCount": 0,
|
||||
},
|
||||
"courseGrade": Object {
|
||||
"courseGrade": {
|
||||
"isPassing": true,
|
||||
"letterGrade": "pass",
|
||||
"percent": 1,
|
||||
@@ -779,10 +779,10 @@ Object {
|
||||
"enrollmentMode": "audit",
|
||||
"gradesFeatureIsFullyLocked": false,
|
||||
"gradesFeatureIsPartiallyLocked": false,
|
||||
"gradingPolicy": Object {
|
||||
"assignmentPolicies": Array [
|
||||
Object {
|
||||
"averageGrade": "1.00",
|
||||
"gradingPolicy": {
|
||||
"assignmentPolicies": [
|
||||
{
|
||||
"averageGrade": "1.0000",
|
||||
"numDroppable": 1,
|
||||
"shortLabel": "HW",
|
||||
"type": "Homework",
|
||||
@@ -790,17 +790,17 @@ Object {
|
||||
"weightedGrade": 1,
|
||||
},
|
||||
],
|
||||
"gradeRange": Object {
|
||||
"gradeRange": {
|
||||
"pass": 0.75,
|
||||
},
|
||||
},
|
||||
"hasScheduledContent": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"sectionScores": Array [
|
||||
Object {
|
||||
"sectionScores": [
|
||||
{
|
||||
"displayName": "First section",
|
||||
"subsections": Array [
|
||||
Object {
|
||||
"subsections": [
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
|
||||
"displayName": "First subsection",
|
||||
@@ -809,16 +809,16 @@ Object {
|
||||
"numPointsEarned": 0,
|
||||
"numPointsPossible": 3,
|
||||
"percentGraded": 0,
|
||||
"problemScores": Array [
|
||||
Object {
|
||||
"problemScores": [
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
@@ -829,18 +829,18 @@ Object {
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"displayName": "Second section",
|
||||
"subsections": Array [
|
||||
Object {
|
||||
"subsections": [
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"displayName": "Second subsection",
|
||||
"hasGradedAssignment": true,
|
||||
"numPointsEarned": 1,
|
||||
"numPointsPossible": 1,
|
||||
"percentGraded": 1,
|
||||
"problemScores": Array [
|
||||
Object {
|
||||
"problemScores": [
|
||||
{
|
||||
"earned": 1,
|
||||
"possible": 1,
|
||||
},
|
||||
@@ -854,7 +854,7 @@ Object {
|
||||
],
|
||||
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
|
||||
"userHasPassingGrade": false,
|
||||
"verificationData": Object {
|
||||
"verificationData": {
|
||||
"link": null,
|
||||
"status": "none",
|
||||
"statusDate": null,
|
||||
@@ -863,16 +863,16 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": Object {},
|
||||
"recommendations": Object {
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": Object {
|
||||
"specialExams": {
|
||||
"activeAttempt": null,
|
||||
"allowProctoringOptOut": false,
|
||||
"apiErrorMsg": "",
|
||||
"exam": Object {
|
||||
"attempt": Object {
|
||||
"exam": {
|
||||
"attempt": {
|
||||
"attempt_code": "",
|
||||
"attempt_id": null,
|
||||
"attempt_status": "",
|
||||
@@ -900,27 +900,27 @@ Object {
|
||||
"is_active": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored": false,
|
||||
"prerequisite_status": Object {
|
||||
"prerequisite_status": {
|
||||
"are_prerequisites_satisifed": true,
|
||||
"declined_prerequisites": Array [],
|
||||
"failed_prerequisites": Array [],
|
||||
"pending_prerequisites": Array [],
|
||||
"satisfied_prerequisites": Array [],
|
||||
"declined_prerequisites": [],
|
||||
"failed_prerequisites": [],
|
||||
"pending_prerequisites": [],
|
||||
"satisfied_prerequisites": [],
|
||||
},
|
||||
"time_limit_mins": null,
|
||||
"type": "",
|
||||
},
|
||||
"examAccessToken": Object {
|
||||
"examAccessToken": {
|
||||
"exam_access_token": "",
|
||||
"exam_access_token_expiration": "",
|
||||
},
|
||||
"isLoading": true,
|
||||
"proctoringSettings": Object {
|
||||
"exam_proctoring_backend": Object {
|
||||
"proctoringSettings": {
|
||||
"exam_proctoring_backend": {
|
||||
"download_url": "",
|
||||
"instructions": Array [],
|
||||
"instructions": [],
|
||||
"name": "",
|
||||
"rules": Object {},
|
||||
"rules": {},
|
||||
},
|
||||
"integration_specific_email": "",
|
||||
"learner_notification_from_email": "",
|
||||
@@ -931,7 +931,7 @@ Object {
|
||||
},
|
||||
"timeIsOver": false,
|
||||
},
|
||||
"tours": Object {
|
||||
"tours": {
|
||||
"showCoursewareTour": false,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
|
||||
@@ -18,7 +18,7 @@ const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) =
|
||||
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
|
||||
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
|
||||
// exists in edx-platform.
|
||||
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(2);
|
||||
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4);
|
||||
weightedGrade = averageGrade * assignmentWeight;
|
||||
}
|
||||
return { averageGrade, weightedGrade };
|
||||
@@ -289,9 +289,17 @@ export async function getProgressTabData(courseId, targetUserId) {
|
||||
}
|
||||
|
||||
export async function getProctoringInfoData(courseId, username) {
|
||||
let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`;
|
||||
if (username) {
|
||||
url += `&username=${encodeURIComponent(username)}`;
|
||||
let url;
|
||||
if (!getConfig().EXAMS_BASE_URL) {
|
||||
url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`;
|
||||
if (username) {
|
||||
url += `&username=${encodeURIComponent(username)}`;
|
||||
}
|
||||
} else {
|
||||
url = `${getConfig().EXAMS_BASE_URL}/api/v1/student/course_id/${encodeURIComponent(courseId)}/onboarding`;
|
||||
if (username) {
|
||||
url += `?username=${encodeURIComponent(username)}`;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
@@ -449,7 +457,7 @@ export async function unsubscribeFromCourseGoal(token) {
|
||||
.then(res => camelCaseObject(res));
|
||||
}
|
||||
|
||||
export async function getCoursewareSearchEnabledFlag(courseId) {
|
||||
export async function getCoursewareSearchEnabled(courseId) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`);
|
||||
const { data } = await getAuthenticatedHttpClient().get(url.href);
|
||||
return { enabled: data.enabled || false };
|
||||
|
||||
@@ -89,6 +89,7 @@ describe('Course Home Service', () => {
|
||||
}),
|
||||
title: string('Demonstration Course'),
|
||||
username: string('edx'),
|
||||
has_course_author_access: boolean(true),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -133,6 +134,7 @@ describe('Course Home Service', () => {
|
||||
],
|
||||
title: 'Demonstration Course',
|
||||
username: 'edx',
|
||||
hasCourseAuthorAccess: true,
|
||||
};
|
||||
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
||||
expect(response).toBeTruthy();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
export const LOADING = 'loading';
|
||||
export const LOADED = 'loaded';
|
||||
export const FAILED = 'failed';
|
||||
export const DENIED = 'denied';
|
||||
import {
|
||||
LOADING,
|
||||
LOADED,
|
||||
FAILED,
|
||||
DENIED,
|
||||
} from '@src/constants';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'course-home',
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
postDismissWelcomeMessage,
|
||||
postRequestCert,
|
||||
getLiveTabIframe,
|
||||
getCoursewareSearchEnabledFlag,
|
||||
getCoursewareSearchEnabled,
|
||||
searchCourseContentFromAPI,
|
||||
} from './api';
|
||||
|
||||
@@ -159,7 +159,7 @@ export function processEvent(eventData, getTabData) {
|
||||
|
||||
export async function fetchCoursewareSearchSettings(courseId) {
|
||||
try {
|
||||
const { enabled } = await getCoursewareSearchEnabledFlag(courseId);
|
||||
const { enabled } = await getCoursewareSearchEnabled(courseId);
|
||||
return { enabled };
|
||||
} catch (e) {
|
||||
return { enabled: false };
|
||||
|
||||
@@ -135,6 +135,7 @@ describe('DatesTab', () => {
|
||||
});
|
||||
|
||||
it('shows extra info', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { items } = await getDay('Sat, Aug 17, 2030');
|
||||
expect(items).toHaveLength(3);
|
||||
|
||||
@@ -142,10 +143,12 @@ describe('DatesTab', () => {
|
||||
const tipText = "ORA Dates are set by the instructor, and can't be changed";
|
||||
|
||||
expect(screen.queryByText(tipText)).toBeNull(); // tooltip does not start in DOM
|
||||
userEvent.hover(tipIcon);
|
||||
const tooltip = screen.getByText(tipText); // now it's there
|
||||
userEvent.unhover(tipIcon);
|
||||
await waitForElementToBeRemoved(tooltip); // and it's gone again
|
||||
await user.hover(tipIcon);
|
||||
screen.getByText(tipText); // now it's there
|
||||
await user.unhover(tipIcon);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(tipText)).toBeNull(); // and it's gone again
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import HeaderSlot from '../../plugin-slots/HeaderSlot';
|
||||
import PageLoading from '../../generic/PageLoading';
|
||||
import { unsubscribeFromCourseGoal } from '../data/api';
|
||||
|
||||
@@ -38,7 +38,7 @@ const GoalUnsubscribe = ({ intl }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header showUserDropdown={false} />
|
||||
<HeaderSlot showUserDropdown={false} />
|
||||
<main id="main-content" className="container my-5 text-center">
|
||||
{isLoading && (
|
||||
<PageLoading srMessage={`${intl.formatMessage(messages.loading)}`} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { AlertList } from '../../generic/user-messages';
|
||||
@@ -15,8 +15,8 @@ import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
|
||||
import CourseTools from './widgets/CourseTools';
|
||||
import { fetchOutlineTab } from '../data';
|
||||
import messages from './messages';
|
||||
import Section from './Section';
|
||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
||||
import useCourseEndAlert from './alerts/course-end-alert';
|
||||
@@ -27,8 +27,10 @@ import { useModel } from '../../generic/model-store';
|
||||
import WelcomeMessage from './widgets/WelcomeMessage';
|
||||
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
||||
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
|
||||
import CourseHomeSectionOutlineSlot from '../../plugin-slots/CourseHomeSectionOutlineSlot';
|
||||
|
||||
const OutlineTab = ({ intl }) => {
|
||||
const OutlineTab = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
proctoringPanelStatus,
|
||||
@@ -38,9 +40,13 @@ const OutlineTab = ({ intl }) => {
|
||||
isSelfPaced,
|
||||
org,
|
||||
title,
|
||||
userTimezone,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const expandButtonRef = useRef();
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
courseBlocks: {
|
||||
courses,
|
||||
sections,
|
||||
@@ -49,12 +55,20 @@ const OutlineTab = ({ intl }) => {
|
||||
selectedGoal,
|
||||
weeklyLearningGoalEnabled,
|
||||
} = {},
|
||||
datesBannerInfo,
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
},
|
||||
enableProctoredExams,
|
||||
offer,
|
||||
timeOffsetMillis,
|
||||
verifiedMode,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const {
|
||||
marketingUrl,
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -148,27 +162,21 @@ const OutlineTab = ({ intl }) => {
|
||||
</>
|
||||
)}
|
||||
<StartOrResumeCourseCard />
|
||||
<WelcomeMessage courseId={courseId} />
|
||||
<WelcomeMessage courseId={courseId} nextElementRef={expandButtonRef} />
|
||||
{rootCourseId && (
|
||||
<>
|
||||
<div className="row w-100 m-0 mb-3 justify-content-end">
|
||||
<div id="expand-button-row" className="row w-100 m-0 mb-3 justify-content-end">
|
||||
<div className="col-12 col-md-auto p-0">
|
||||
<Button variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
|
||||
<Button ref={expandButtonRef} variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
|
||||
{expandAll ? intl.formatMessage(messages.collapseAll) : intl.formatMessage(messages.expandAll)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ol id="courseHome-outline" className="list-unstyled">
|
||||
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
||||
<Section
|
||||
key={sectionId}
|
||||
courseId={courseId}
|
||||
defaultOpen={sections[sectionId].resumeBlock}
|
||||
expand={expandAll}
|
||||
section={sections[sectionId]}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
<CourseHomeSectionOutlineSlot
|
||||
expandAll={expandAll}
|
||||
sectionIds={courses[rootCourseId].sectionIds}
|
||||
sections={sections}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -186,8 +194,25 @@ const OutlineTab = ({ intl }) => {
|
||||
<CourseTools />
|
||||
<PluginSlot
|
||||
id="outline_tab_notifications_slot"
|
||||
pluginProps={{ courseId }}
|
||||
/>
|
||||
pluginProps={{
|
||||
courseId,
|
||||
model: 'outline',
|
||||
}}
|
||||
>
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="course_home"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
/>
|
||||
</PluginSlot>
|
||||
<CourseDates />
|
||||
<CourseHandouts />
|
||||
</div>
|
||||
@@ -197,8 +222,4 @@ const OutlineTab = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
OutlineTab.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(OutlineTab);
|
||||
export default OutlineTab;
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Cookies from 'js-cookie';
|
||||
@@ -54,7 +54,7 @@ describe('Outline Tab', () => {
|
||||
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
||||
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
||||
const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
||||
const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}&username=MockUser`;
|
||||
const proctoringInfoUrl = `${getConfig().EXAMS_BASE_URL}/api/v1/student/course_id/${encodeURIComponent(courseId)}/onboarding?username=MockUser`;
|
||||
|
||||
const store = initializeStore();
|
||||
const defaultMetadata = Factory.build('courseHomeMetadata');
|
||||
@@ -143,6 +143,7 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
|
||||
it('handles expand/collapse all button click', async () => {
|
||||
const user = userEvent.setup();
|
||||
await fetchAndRender();
|
||||
// Button renders as "Expand All"
|
||||
const expandButton = screen.getByRole('button', { name: 'Expand all' });
|
||||
@@ -153,11 +154,11 @@ describe('Outline Tab', () => {
|
||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
// Click to expand section
|
||||
userEvent.click(expandButton);
|
||||
await user.click(expandButton);
|
||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
||||
|
||||
// Click to collapse section
|
||||
userEvent.click(expandButton);
|
||||
await user.click(expandButton);
|
||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
||||
});
|
||||
|
||||
@@ -167,7 +168,7 @@ describe('Outline Tab', () => {
|
||||
course_blocks: { blocks: courseBlocks.blocks },
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByTitle('Completed section')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Completed section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays correct icon for incomplete assignment', async () => {
|
||||
@@ -176,7 +177,7 @@ describe('Outline Tab', () => {
|
||||
course_blocks: { blocks: courseBlocks.blocks },
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Incomplete section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('SequenceLink displays link', async () => {
|
||||
@@ -275,21 +276,34 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
|
||||
it('renders show more/less button and handles click', async () => {
|
||||
const user = userEvent.setup();
|
||||
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
||||
let showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
||||
expect(showMoreButton).toBeInTheDocument();
|
||||
|
||||
userEvent.click(showMoreButton);
|
||||
await user.click(showMoreButton);
|
||||
let showLessButton = screen.getByRole('button', { name: 'Show Less' });
|
||||
expect(showLessButton).toBeInTheDocument();
|
||||
expect(screen.getByTestId('long-welcome-message-iframe')).toBeInTheDocument();
|
||||
|
||||
userEvent.click(showLessButton);
|
||||
await user.click(showLessButton);
|
||||
showLessButton = screen.queryByRole('button', { name: 'Show Less' });
|
||||
expect(showLessButton).not.toBeInTheDocument();
|
||||
showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
||||
expect(showMoreButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('dismisses message', async () => {
|
||||
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
||||
const dismissButton = screen.queryByRole('button', { name: 'Dismiss' });
|
||||
const expandButton = screen.queryByRole('button', { name: 'Expand all' });
|
||||
|
||||
fireEvent.click(dismissButton);
|
||||
|
||||
expect(expandButton).toHaveFocus();
|
||||
|
||||
expect(screen.queryByText('Welcome Message')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores comments and misformatted HTML', async () => {
|
||||
@@ -1176,6 +1190,80 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upgrade Card', () => {
|
||||
it('renders title when upgrade is available', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays link to upgrade', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('viewing upgrade card sends analytics', async () => {
|
||||
sendTrackEvent.mockClear();
|
||||
sendTrackingLogEvent.mockClear();
|
||||
await fetchAndRender();
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('Promotion Viewed', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
creative: 'sidebarupsell',
|
||||
name: 'In-Course Verification Prompt',
|
||||
position: 'sidebar-message',
|
||||
promotion_id: 'courseware_verified_certificate_upsell',
|
||||
});
|
||||
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking upgrade link sends analytics', async () => {
|
||||
await fetchAndRender();
|
||||
|
||||
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
|
||||
sendTrackEvent.mockClear();
|
||||
sendTrackingLogEvent.mockClear();
|
||||
const upgradeButton = screen.getByRole('link', { name: 'Upgrade for $149' });
|
||||
|
||||
fireEvent.click(upgradeButton);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
creative: 'sidebarupsell',
|
||||
name: 'In-Course Verification Prompt',
|
||||
position: 'sidebar-message',
|
||||
promotion_id: 'courseware_verified_certificate_upsell',
|
||||
});
|
||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
linkCategory: 'green_upgrade',
|
||||
linkName: 'course_home_green',
|
||||
linkType: 'button',
|
||||
pageName: 'course_home',
|
||||
});
|
||||
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2);
|
||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
});
|
||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
location: 'sidebar-message',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Account Activation Alert', () => {
|
||||
beforeEach(() => {
|
||||
const intersectionObserverMock = () => ({
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, IconButton, Icon } from '@openedx/paragon';
|
||||
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { DisabledVisible } from '@openedx/paragon/icons';
|
||||
import SequenceLink from './SequenceLink';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
import genericMessages from '../../generic/messages';
|
||||
import messages from './messages';
|
||||
|
||||
const Section = ({
|
||||
courseId,
|
||||
defaultOpen,
|
||||
expand,
|
||||
intl,
|
||||
section,
|
||||
}) => {
|
||||
const {
|
||||
complete,
|
||||
sequenceIds,
|
||||
title,
|
||||
hideFromTOC,
|
||||
} = section;
|
||||
const {
|
||||
courseBlocks: {
|
||||
sequences,
|
||||
},
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(expand);
|
||||
}, [expand]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(defaultOpen);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const sectionTitle = (
|
||||
<div className="d-flex row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left mt-1 text-success"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.completedSection)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left mt-1 text-gray-400"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.incompleteSection)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500">
|
||||
<span className="align-middle col-6">{title}</span>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
|
||||
</span>
|
||||
</div>
|
||||
{hideFromTOC && (
|
||||
<div className="row">
|
||||
{hideFromTOC && (
|
||||
<span className="small d-flex align-content-end">
|
||||
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
|
||||
<span data-testid="hide-from-toc-section-text">
|
||||
{intl.formatMessage(messages.hiddenSection)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Collapsible
|
||||
className="mb-2"
|
||||
styling="card-lg"
|
||||
title={sectionTitle}
|
||||
open={open}
|
||||
onToggle={() => { setOpen(!open); }}
|
||||
iconWhenClosed={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.openSection)}
|
||||
icon={faPlus}
|
||||
onClick={() => { setOpen(true); }}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
iconWhenOpen={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(genericMessages.close)}
|
||||
icon={faMinus}
|
||||
onClick={() => { setOpen(false); }}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<ol className="list-unstyled">
|
||||
{sequenceIds.map((sequenceId, index) => (
|
||||
<SequenceLink
|
||||
key={sequenceId}
|
||||
id={sequenceId}
|
||||
courseId={courseId}
|
||||
sequence={sequences[sequenceId]}
|
||||
first={index === 0}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
Section.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
defaultOpen: PropTypes.bool.isRequired,
|
||||
expand: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
section: PropTypes.shape().isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Section);
|
||||
@@ -1,147 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
FormattedMessage,
|
||||
FormattedTime,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Block } from '@openedx/paragon/icons';
|
||||
import EffortEstimate from '../../shared/effort-estimate';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
const SequenceLink = ({
|
||||
id,
|
||||
intl,
|
||||
courseId,
|
||||
first,
|
||||
sequence,
|
||||
}) => {
|
||||
const {
|
||||
complete,
|
||||
description,
|
||||
due,
|
||||
showLink,
|
||||
title,
|
||||
hideFromTOC,
|
||||
} = sequence;
|
||||
const {
|
||||
userTimezone,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
||||
const displayTitle = showLink ? coursewareUrl : title;
|
||||
|
||||
const dueDateMessage = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due-date-set"
|
||||
defaultMessage="{description} due {assignmentDue}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const noDueDateMessage = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due-date-not-set"
|
||||
defaultMessage="{description}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-success mt-1"
|
||||
aria-hidden={complete}
|
||||
title={intl.formatMessage(messages.completedAssignment)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-gray-400 mt-1"
|
||||
aria-hidden={complete}
|
||||
title={intl.formatMessage(messages.incompleteAssignment)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-10 p-0 ml-3 text-break">
|
||||
<span className="align-middle">{displayTitle}</span>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
|
||||
</span>
|
||||
<EffortEstimate className="ml-3 align-middle" block={sequence} />
|
||||
</div>
|
||||
</div>
|
||||
{hideFromTOC && (
|
||||
<div className="row w-100 my-2 mx-4 pl-3">
|
||||
<span className="small d-flex">
|
||||
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
|
||||
<span data-testid="hide-from-toc-sequence-link-text">
|
||||
{intl.formatMessage(messages.hiddenSequenceLink)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="row w-100 m-0 ml-3 pl-3">
|
||||
<small className="text-body pl-2">
|
||||
{due ? dueDateMessage : noDueDateMessage}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
SequenceLink.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
first: PropTypes.bool.isRequired,
|
||||
sequence: PropTypes.shape().isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SequenceLink);
|
||||
@@ -341,6 +341,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Onboarding Past Due',
|
||||
description: 'Text that show when the deadline of proctortrack onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason',
|
||||
},
|
||||
sequenceDueDate: {
|
||||
id: 'learning.outline.sequence-due-date-set',
|
||||
defaultMessage: '{description} due {assignmentDue}',
|
||||
description: 'Used below an assignment title',
|
||||
},
|
||||
sequenceNoDueDate: {
|
||||
id: 'learning.outline.sequence-due-date-not-set',
|
||||
defaultMessage: '{description}',
|
||||
description: 'Used below an assignment title',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Block } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
interface Props {}
|
||||
|
||||
const HiddenSequenceLink: React.FC<Props> = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className="row w-100 my-2 mx-4 pl-3">
|
||||
<span className="small d-flex">
|
||||
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
|
||||
<span data-testid="hide-from-toc-sequence-link-text">
|
||||
{intl.formatMessage(messages.hiddenSequenceLink)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HiddenSequenceLink;
|
||||
94
src/course-home/outline-tab/section-outline/Section.tsx
Normal file
94
src/course-home/outline-tab/section-outline/Section.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, IconButton } from '@openedx/paragon';
|
||||
import { Minus, Plus } from '@openedx/paragon/icons';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import genericMessages from '../../../generic/messages';
|
||||
import { useContextId } from '../../../data/hooks';
|
||||
import messages from '../messages';
|
||||
import SectionTitle from './SectionTitle';
|
||||
import SequenceLink from './SequenceLink';
|
||||
|
||||
interface Props {
|
||||
defaultOpen: boolean;
|
||||
expand: boolean;
|
||||
section: {
|
||||
complete: boolean;
|
||||
sequenceIds: string[];
|
||||
title: string;
|
||||
hideFromTOC: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const Section: React.FC<Props> = ({
|
||||
defaultOpen,
|
||||
expand,
|
||||
section,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
const {
|
||||
complete,
|
||||
sequenceIds,
|
||||
title,
|
||||
hideFromTOC,
|
||||
} = section;
|
||||
const {
|
||||
courseBlocks: {
|
||||
sequences,
|
||||
},
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(expand);
|
||||
}, [expand]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(defaultOpen);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Collapsible
|
||||
className="mb-2"
|
||||
styling="card-lg"
|
||||
title={<SectionTitle {...{ complete, hideFromTOC, title }} />}
|
||||
open={open}
|
||||
onToggle={() => { setOpen(!open); }}
|
||||
iconWhenClosed={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.openSection)}
|
||||
iconAs={Plus}
|
||||
onClick={() => { setOpen(true); }}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
iconWhenOpen={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(genericMessages.close)}
|
||||
iconAs={Minus}
|
||||
onClick={() => { setOpen(false); }}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<ol className="list-unstyled">
|
||||
{sequenceIds.map((sequenceId, index) => (
|
||||
<SequenceLink
|
||||
key={sequenceId}
|
||||
id={sequenceId}
|
||||
sequence={sequences[sequenceId]}
|
||||
first={index === 0}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Section;
|
||||
59
src/course-home/outline-tab/section-outline/SectionTitle.tsx
Normal file
59
src/course-home/outline-tab/section-outline/SectionTitle.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { CheckCircle, CheckCircleOutline, DisabledVisible } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
interface Props {
|
||||
complete: boolean;
|
||||
hideFromTOC: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const SectionTitle: React.FC<Props> = ({ complete, hideFromTOC, title }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="d-flex row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<Icon
|
||||
src={CheckCircle}
|
||||
className="float-left mt-1 text-success"
|
||||
aria-hidden="true"
|
||||
svgAttrs={{ 'aria-label': intl.formatMessage(messages.completedSection) }}
|
||||
size="sm"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
src={CheckCircleOutline}
|
||||
className="float-left mt-1 text-gray-400"
|
||||
aria-hidden="true"
|
||||
svgAttrs={{ 'aria-label': intl.formatMessage(messages.incompleteSection) }}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500">
|
||||
<span className="align-middle col-6">{title}</span>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
|
||||
</span>
|
||||
</div>
|
||||
{hideFromTOC && (
|
||||
<div className="row">
|
||||
{hideFromTOC && (
|
||||
<span className="small d-flex align-content-end">
|
||||
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
|
||||
<span data-testid="hide-from-toc-section-text">
|
||||
{intl.formatMessage(messages.hiddenSection)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTitle;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { FormattedTime, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import { useContextId } from '../../../data/hooks';
|
||||
import messages from '../messages';
|
||||
|
||||
interface Props {
|
||||
due: string;
|
||||
id: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SequenceDueDate: React.FC<Props> = ({
|
||||
due,
|
||||
id,
|
||||
description,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
let dueDateMessage: string | React.ReactNode = intl.formatMessage(
|
||||
messages.sequenceNoDueDate,
|
||||
{ description: description || '' },
|
||||
);
|
||||
const {
|
||||
userTimezone,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
if (due) {
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
dueDateMessage = intl.formatMessage(
|
||||
messages.sequenceDueDate,
|
||||
{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row w-100 m-0 ml-3 pl-3">
|
||||
<small className="text-body pl-2">
|
||||
{dueDateMessage}
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SequenceDueDate;
|
||||
56
src/course-home/outline-tab/section-outline/SequenceLink.tsx
Normal file
56
src/course-home/outline-tab/section-outline/SequenceLink.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import SequenceDueDate from './SequenceDueDate';
|
||||
import HiddenSequenceLink from './HiddenSequenceLink';
|
||||
import SequenceTitle from './SequenceTitle';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
first: boolean;
|
||||
sequence: {
|
||||
complete: boolean;
|
||||
description: string;
|
||||
due: string;
|
||||
showLink: boolean;
|
||||
title: string;
|
||||
hideFromTOC: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const SequenceLink: React.FC<Props> = ({
|
||||
id,
|
||||
first,
|
||||
sequence,
|
||||
}) => {
|
||||
const {
|
||||
complete,
|
||||
description,
|
||||
due,
|
||||
showLink,
|
||||
title,
|
||||
hideFromTOC,
|
||||
} = sequence;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||
<SequenceTitle
|
||||
{...{
|
||||
complete,
|
||||
showLink,
|
||||
title,
|
||||
sequence,
|
||||
id,
|
||||
}}
|
||||
/>
|
||||
{hideFromTOC && (
|
||||
<HiddenSequenceLink />
|
||||
)}
|
||||
<SequenceDueDate {...{ due, id, description }} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default SequenceLink;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { CheckCircleOutline, CheckCircle } from '@openedx/paragon/icons';
|
||||
|
||||
import EffortEstimate from '../../../shared/effort-estimate';
|
||||
import messages from '../messages';
|
||||
import { useContextId } from '../../../data/hooks';
|
||||
|
||||
interface Props {
|
||||
complete: boolean;
|
||||
showLink: boolean;
|
||||
title: string;
|
||||
sequence: object;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const SequenceTitle: React.FC<Props> = ({
|
||||
complete,
|
||||
showLink,
|
||||
title,
|
||||
sequence,
|
||||
id,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
||||
const displayTitle = showLink ? coursewareUrl : title;
|
||||
|
||||
return (
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<Icon
|
||||
src={CheckCircle}
|
||||
className="float-left text-success mt-1"
|
||||
aria-hidden={complete}
|
||||
svgAttrs={{ 'aria-label': intl.formatMessage(messages.completedAssignment) }}
|
||||
size="sm"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
src={CheckCircleOutline}
|
||||
className="float-left text-gray-400 mt-1"
|
||||
aria-hidden={complete}
|
||||
svgAttrs={{ 'aria-label': intl.formatMessage(messages.incompleteAssignment) }}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-10 p-0 ml-3 text-break">
|
||||
<span className="align-middle">{displayTitle}</span>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
|
||||
</span>
|
||||
<EffortEstimate className="ml-3 align-middle" block={sequence} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SequenceTitle;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button, TransitionReplace } from '@openedx/paragon';
|
||||
import truncate from 'truncate-html';
|
||||
|
||||
@@ -11,11 +11,13 @@ import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import { dismissWelcomeMessage } from '../../data/thunks';
|
||||
|
||||
const WelcomeMessage = ({ courseId, intl }) => {
|
||||
const WelcomeMessage = ({ courseId, nextElementRef }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
welcomeMessageHtml,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const messageBodyRef = useRef();
|
||||
const [display, setDisplay] = useState(true);
|
||||
|
||||
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
|
||||
@@ -49,13 +51,20 @@ const WelcomeMessage = ({ courseId, intl }) => {
|
||||
dismissible
|
||||
show={display}
|
||||
onClose={() => {
|
||||
nextElementRef.current?.focus();
|
||||
setDisplay(false);
|
||||
dispatch(dismissWelcomeMessage(courseId));
|
||||
}}
|
||||
className="raised-card"
|
||||
actions={messageCanBeShortened ? [
|
||||
<Button
|
||||
onClick={() => setShowShortMessage(!showShortMessage)}
|
||||
onClick={() => {
|
||||
if (showShortMessage) {
|
||||
messageBodyRef.current?.focus();
|
||||
}
|
||||
|
||||
setShowShortMessage(!showShortMessage);
|
||||
}}
|
||||
variant="outline-primary"
|
||||
>
|
||||
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
|
||||
@@ -63,32 +72,34 @@ const WelcomeMessage = ({ courseId, intl }) => {
|
||||
</Button>,
|
||||
] : []}
|
||||
>
|
||||
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
|
||||
{showShortMessage ? (
|
||||
<LmsHtmlFragment
|
||||
className="inline-link"
|
||||
data-testid="short-welcome-message-iframe"
|
||||
key="short-html"
|
||||
html={shortWelcomeMessageHtml}
|
||||
title={intl.formatMessage(messages.welcomeMessage)}
|
||||
/>
|
||||
) : (
|
||||
<LmsHtmlFragment
|
||||
className="inline-link"
|
||||
data-testid="long-welcome-message-iframe"
|
||||
key="full-html"
|
||||
html={cleanedWelcomeMessageHtml}
|
||||
title={intl.formatMessage(messages.welcomeMessage)}
|
||||
/>
|
||||
)}
|
||||
</TransitionReplace>
|
||||
<div ref={messageBodyRef} tabIndex="-1">
|
||||
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
|
||||
{showShortMessage ? (
|
||||
<LmsHtmlFragment
|
||||
className="inline-link"
|
||||
data-testid="short-welcome-message-iframe"
|
||||
key="short-html"
|
||||
html={shortWelcomeMessageHtml}
|
||||
title={intl.formatMessage(messages.welcomeMessage)}
|
||||
/>
|
||||
) : (
|
||||
<LmsHtmlFragment
|
||||
className="inline-link"
|
||||
data-testid="long-welcome-message-iframe"
|
||||
key="full-html"
|
||||
html={cleanedWelcomeMessageHtml}
|
||||
title={intl.formatMessage(messages.welcomeMessage)}
|
||||
/>
|
||||
)}
|
||||
</TransitionReplace>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
WelcomeMessage.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
nextElementRef: PropTypes.shape({ current: PropTypes.instanceOf(HTMLInputElement) }),
|
||||
};
|
||||
|
||||
export default injectIntl(WelcomeMessage);
|
||||
export default WelcomeMessage;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ProgressHeader = ({ intl }) => {
|
||||
const ProgressHeader = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
targetUserId,
|
||||
@@ -37,8 +36,4 @@ const ProgressHeader = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
ProgressHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ProgressHeader);
|
||||
export default ProgressHeader;
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
import { useWindowSize } from '@openedx/paragon';
|
||||
import { useContextId } from '../../data/hooks';
|
||||
import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot';
|
||||
|
||||
import CertificateStatus from './certificate-status/CertificateStatus';
|
||||
import CourseCompletion from './course-completion/CourseCompletion';
|
||||
import CourseGrade from './grades/course-grade/CourseGrade';
|
||||
import DetailedGrades from './grades/detailed-grades/DetailedGrades';
|
||||
import GradeSummary from './grades/grade-summary/GradeSummary';
|
||||
import ProgressHeader from './ProgressHeader';
|
||||
import RelatedLinks from './related-links/RelatedLinks';
|
||||
|
||||
import ProgressTabCertificateStatusMainBodySlot from '../../plugin-slots/ProgressTabCertificateStatusMainBodySlot';
|
||||
import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot';
|
||||
import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot';
|
||||
import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
const ProgressTab = () => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
|
||||
const {
|
||||
gradesFeatureIsFullyLocked, disableProgressGraph,
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
||||
const courseId = useContextId();
|
||||
const { disableProgressGraph } = useModel('progress', courseId);
|
||||
|
||||
const windowWidth = useWindowSize().width;
|
||||
if (windowWidth === undefined) {
|
||||
@@ -31,7 +24,6 @@ const ProgressTab = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const wideScreen = windowWidth >= breakpoints.large.minWidth;
|
||||
return (
|
||||
<>
|
||||
<ProgressHeader />
|
||||
@@ -39,18 +31,15 @@ const ProgressTab = () => {
|
||||
{/* Main body */}
|
||||
<div className="col-12 col-md-8 p-0">
|
||||
{!disableProgressGraph && <CourseCompletion />}
|
||||
{!wideScreen && <CertificateStatus />}
|
||||
<CourseGrade />
|
||||
<div className={`grades my-4 p-4 rounded raised-card ${applyLockedOverlay}`} aria-hidden={gradesFeatureIsFullyLocked}>
|
||||
<GradeSummary />
|
||||
<DetailedGrades />
|
||||
</div>
|
||||
<ProgressTabCertificateStatusMainBodySlot />
|
||||
<ProgressTabCourseGradeSlot />
|
||||
<ProgressTabGradeBreakdownSlot />
|
||||
</div>
|
||||
|
||||
{/* Side panel */}
|
||||
<div className="col-12 col-md-4 p-0 px-md-4">
|
||||
{wideScreen && <CertificateStatus />}
|
||||
<RelatedLinks />
|
||||
<ProgressTabCertificateStatusSidePanelSlot />
|
||||
<ProgressTabRelatedLinksSlot />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
@@ -111,7 +111,7 @@ describe('Progress Tab', () => {
|
||||
await fetchAndRender();
|
||||
sendTrackEvent.mockClear();
|
||||
|
||||
const outlineTabLink = screen.getAllByRole('link', { name: 'Course Outline' });
|
||||
const outlineTabLink = screen.getAllByRole('link', { name: 'Course outline' });
|
||||
fireEvent.click(outlineTabLink[1]); // outlineTabLink[0] corresponds to the link in the DetailedGrades component
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
@@ -471,9 +471,12 @@ describe('Progress Tab', () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('limited feature')).toBeInTheDocument();
|
||||
expect(screen.getByText('Unlock to work towards a certificate.')).toBeInTheDocument();
|
||||
expect(screen.queryAllByText('You have limited access to graded assignments as part of the audit track in this course.')).toHaveLength(2);
|
||||
expect(screen.queryAllByText(
|
||||
'You have limited access to graded assignments as part of the audit track in this course.',
|
||||
{ exact: false },
|
||||
)).toHaveLength(2);
|
||||
|
||||
expect(screen.queryAllByTestId('blocked-icon')).toHaveLength(4);
|
||||
expect(screen.queryAllByTestId('locked-icon')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('does not render subsections for which showGrades is false', async () => {
|
||||
@@ -545,6 +548,111 @@ describe('Progress Tab', () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render ungraded subsections when SHOW_UNGRADED_ASSIGNMENT_PROGRESS is false', async () => {
|
||||
// The second assignment has has_graded_assignment set to false, so it should not be shown.
|
||||
setTabData({
|
||||
section_scores: [
|
||||
{
|
||||
display_name: 'First section',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Homework',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||
display_name: 'First subsection',
|
||||
learner_has_access: true,
|
||||
has_graded_assignment: true,
|
||||
num_points_earned: 1,
|
||||
num_points_possible: 2,
|
||||
percent_graded: 1.0,
|
||||
show_correctness: 'always',
|
||||
show_grades: true,
|
||||
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display_name: 'Second section',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Homework',
|
||||
display_name: 'Second subsection',
|
||||
learner_has_access: true,
|
||||
has_graded_assignment: false,
|
||||
num_points_earned: 1,
|
||||
num_points_possible: 1,
|
||||
percent_graded: 1.0,
|
||||
show_correctness: 'always',
|
||||
show_grades: true,
|
||||
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('First subsection')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Second subsection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders both graded and ungraded subsections when SHOW_UNGRADED_ASSIGNMENT_PROGRESS is true', async () => {
|
||||
// The second assignment has has_graded_assignment set to false.
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS: true,
|
||||
});
|
||||
|
||||
setTabData({
|
||||
section_scores: [
|
||||
{
|
||||
display_name: 'First section',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Homework',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||
display_name: 'First subsection',
|
||||
learner_has_access: true,
|
||||
has_graded_assignment: true,
|
||||
num_points_earned: 1,
|
||||
num_points_possible: 2,
|
||||
percent_graded: 1.0,
|
||||
show_correctness: 'always',
|
||||
show_grades: true,
|
||||
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display_name: 'Second section',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Homework',
|
||||
display_name: 'Second subsection',
|
||||
learner_has_access: true,
|
||||
has_graded_assignment: false,
|
||||
num_points_earned: 1,
|
||||
num_points_possible: 1,
|
||||
percent_graded: 1.0,
|
||||
show_correctness: 'always',
|
||||
show_grades: true,
|
||||
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('First subsection')).toBeInTheDocument();
|
||||
expect(screen.getByText('Second subsection')).toBeInTheDocument();
|
||||
|
||||
// reset config for other tests
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Grade Summary', () => {
|
||||
@@ -788,7 +896,7 @@ describe('Progress Tab', () => {
|
||||
sendTrackEvent.mockClear();
|
||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||
|
||||
const outlineLink = screen.getAllByRole('link', { name: 'Course Outline' })[0];
|
||||
const outlineLink = screen.getAllByRole('link', { name: 'Course outline' })[0];
|
||||
fireEvent.click(outlineLink);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
@@ -809,7 +917,7 @@ describe('Progress Tab', () => {
|
||||
|
||||
// Open the problem score drawer
|
||||
fireEvent.click(problemScoreDrawerToggle);
|
||||
expect(screen.getByText('Problem Scores:')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Graded Scores:').length).toBeGreaterThan(1);
|
||||
expect(screen.getAllByText('0/1')).toHaveLength(3);
|
||||
});
|
||||
|
||||
@@ -821,6 +929,14 @@ describe('Progress Tab', () => {
|
||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||
expect(screen.getByText('You currently have no graded problem scores.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Detailed Grades table when section scores are populated', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('First subsection'));
|
||||
expect(screen.getByText('Second subsection'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate Status', () => {
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Button, Card } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useContextId } from '../../../data/hooks';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
|
||||
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
|
||||
import { requestCert } from '../../data/thunks';
|
||||
import messages from './messages';
|
||||
import ProgressCertificateStatusSlot from '../../../plugin-slots/ProgressCertificateStatusSlot';
|
||||
|
||||
const CertificateStatus = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const CertificateStatus = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
entranceExamData,
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
|
||||
const {
|
||||
isEnrolled,
|
||||
@@ -42,6 +45,8 @@ const CertificateStatus = ({ intl }) => {
|
||||
certificateAvailableDate,
|
||||
} = certificateData || {};
|
||||
|
||||
const entranceExamPassed = entranceExamData?.entranceExamPassed ?? null;
|
||||
|
||||
const mode = getCourseExitMode(
|
||||
certificateData,
|
||||
hasScheduledContent,
|
||||
@@ -49,6 +54,7 @@ const CertificateStatus = ({ intl }) => {
|
||||
userHasPassingGrade,
|
||||
null, // CourseExitPageIsActive
|
||||
canViewCertificate,
|
||||
entranceExamPassed,
|
||||
);
|
||||
|
||||
const eventProperties = {
|
||||
@@ -208,7 +214,6 @@ const CertificateStatus = ({ intl }) => {
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (!certCase) {
|
||||
return null;
|
||||
}
|
||||
@@ -236,32 +241,32 @@ const CertificateStatus = ({ intl }) => {
|
||||
return (
|
||||
<section data-testid="certificate-status-component" className="text-dark-700 mb-4">
|
||||
<Card className="bg-light-200 raised-card">
|
||||
<Card.Header title={header} />
|
||||
<Card.Section className="small text-gray-700">
|
||||
{body}
|
||||
</Card.Section>
|
||||
<Card.Footer>
|
||||
{buttonText && (buttonLocation || buttonAction) && (
|
||||
<Button
|
||||
variant="outline-brand"
|
||||
onClick={() => {
|
||||
logCertificateStatusButtonClicked(certStatus);
|
||||
if (buttonAction) { buttonAction(); }
|
||||
}}
|
||||
href={buttonLocation}
|
||||
block
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</Card.Footer>
|
||||
<ProgressCertificateStatusSlot courseId={courseId}>
|
||||
<div id={`${certCase}_certificate_status`}>
|
||||
<Card.Header title={header} />
|
||||
<Card.Section className="small text-gray-700">
|
||||
{body}
|
||||
</Card.Section>
|
||||
<Card.Footer>
|
||||
{buttonText && (buttonLocation || buttonAction) && (
|
||||
<Button
|
||||
variant="outline-brand"
|
||||
onClick={() => {
|
||||
logCertificateStatusButtonClicked(certStatus);
|
||||
if (buttonAction) { buttonAction(); }
|
||||
}}
|
||||
href={buttonLocation}
|
||||
block
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</Card.Footer>
|
||||
</div>
|
||||
</ProgressCertificateStatusSlot>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
CertificateStatus.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CertificateStatus);
|
||||
export default CertificateStatus;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) => {
|
||||
const CompleteDonutSegment = ({ completePercentage, lockedPercentage }) => {
|
||||
const intl = useIntl();
|
||||
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
||||
|
||||
if (!completePercentage) {
|
||||
@@ -82,8 +83,7 @@ const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) =>
|
||||
|
||||
CompleteDonutSegment.propTypes = {
|
||||
completePercentage: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
lockedPercentage: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CompleteDonutSegment);
|
||||
export default CompleteDonutSegment;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useContextId } from '../../../data/hooks';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import CompleteDonutSegment from './CompleteDonutSegment';
|
||||
@@ -10,10 +7,9 @@ import IncompleteDonutSegment from './IncompleteDonutSegment';
|
||||
import LockedDonutSegment from './LockedDonutSegment';
|
||||
import messages from './messages';
|
||||
|
||||
const CompletionDonutChart = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const CompletionDonutChart = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
completionSummary: {
|
||||
@@ -62,8 +58,4 @@ const CompletionDonutChart = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CompletionDonutChart.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CompletionDonutChart);
|
||||
export default CompletionDonutChart;
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import CompletionDonutChart from './CompletionDonutChart';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseCompletion = ({ intl }) => (
|
||||
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||
<p className="small">
|
||||
{intl.formatMessage(messages.completionBody)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
||||
<CompletionDonutChart />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
const CourseCompletion = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
CourseCompletion.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
return (
|
||||
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||
<p className="small">
|
||||
{intl.formatMessage(messages.completionBody)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
||||
<CompletionDonutChart />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(CourseCompletion);
|
||||
export default CourseCompletion;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
||||
const IncompleteDonutSegment = ({ incompletePercentage }) => {
|
||||
const intl = useIntl();
|
||||
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
||||
|
||||
if (!incompletePercentage) {
|
||||
@@ -53,7 +54,6 @@ const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
||||
|
||||
IncompleteDonutSegment.propTypes = {
|
||||
incompletePercentage: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(IncompleteDonutSegment);
|
||||
export default IncompleteDonutSegment;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
||||
const LockedDonutSegment = ({ lockedPercentage }) => {
|
||||
const intl = useIntl();
|
||||
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
||||
|
||||
if (!lockedPercentage) {
|
||||
@@ -65,8 +66,7 @@ const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
||||
};
|
||||
|
||||
LockedDonutSegment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
lockedPercentage: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LockedDonutSegment);
|
||||
export default LockedDonutSegment;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CheckCircle, WarningFilled, WatchFilled } from '@openedx/paragon/icons';
|
||||
import { Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../data/hooks';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import { DashboardLink } from '../../../shared/links';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const CreditInformation = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const CreditInformation = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
creditCourseRequirements,
|
||||
@@ -36,36 +34,13 @@ const CreditInformation = ({ intl }) => {
|
||||
|
||||
switch (creditCourseRequirements.eligibilityStatus) {
|
||||
case 'not_eligible':
|
||||
eligibilityStatus = (
|
||||
<FormattedMessage
|
||||
id="progress.creditInformation.creditNotEligible"
|
||||
defaultMessage="You are no longer eligible for credit in this course. Learn more about {creditLink}."
|
||||
description="Message to learner who are not eligible for course credit, it can because the a requirement deadline have passed"
|
||||
values={{ creditLink }}
|
||||
/>
|
||||
);
|
||||
eligibilityStatus = intl.formatMessage(messages.creditNotEligibleStatus, { creditLink });
|
||||
break;
|
||||
case 'eligible':
|
||||
eligibilityStatus = (
|
||||
<FormattedMessage
|
||||
id="progress.creditInformation.creditEligible"
|
||||
defaultMessage="
|
||||
You have met the requirements for credit in this course. Go to your
|
||||
{dashboardLink} to purchase course credit. Or learn more about {creditLink}."
|
||||
description="After the credit requirements are met, leaners can then do the last step which purchasing the credit. Note that is only doable for leaners after they met all the requirements"
|
||||
values={{ dashboardLink, creditLink }}
|
||||
/>
|
||||
);
|
||||
eligibilityStatus = intl.formatMessage(messages.creditEligibleStatus, { dashboardLink, creditLink });
|
||||
break;
|
||||
case 'partial_eligible':
|
||||
eligibilityStatus = (
|
||||
<FormattedMessage
|
||||
id="progress.creditInformation.creditPartialEligible"
|
||||
defaultMessage="You have not yet met the requirements for credit. Learn more about {creditLink}."
|
||||
description="This means that one or more requirements is not satisfied yet"
|
||||
values={{ creditLink }}
|
||||
/>
|
||||
);
|
||||
eligibilityStatus = intl.formatMessage(messages.creditPartialEligibleStatus, { creditLink });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -108,8 +83,4 @@ const CreditInformation = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CreditInformation.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CreditInformation);
|
||||
export default CreditInformation;
|
||||
|
||||
@@ -35,6 +35,22 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Verification submitted',
|
||||
description: 'It indicate that the learner submitted a requirement but is not graded or reviewed yet',
|
||||
},
|
||||
creditNotEligibleStatus: {
|
||||
id: 'progress.creditInformation.creditNotEligible',
|
||||
defaultMessage: 'You are no longer eligible for credit in this course. Learn more about {creditLink}.',
|
||||
description: 'Message to learner who are not eligible for course credit, it can be that a requirement deadline has passed',
|
||||
},
|
||||
creditEligibleStatus: {
|
||||
id: 'progress.creditInformation.creditEligible',
|
||||
defaultMessage: `You have met the requirements for credit in this course. Go to your
|
||||
{dashboardLink} to purchase course credit. Or learn more about {creditLink}.`,
|
||||
description: 'After the credit requirements are met, leaners can then do the last step which purchasing the credit. Note that is only doable for leaners after they met all the requirements',
|
||||
},
|
||||
creditPartialEligibleStatus: {
|
||||
id: 'progress.creditInformation.creditPartialEligible',
|
||||
defaultMessage: 'You have not yet met the requirements for credit. Learn more about {creditLink}.',
|
||||
description: 'This means that one or more requirements is not satisfied yet',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
@@ -11,10 +10,9 @@ import CreditInformation from '../../credit-information/CreditInformation';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const CourseGrade = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const CourseGrade = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
creditCourseRequirements,
|
||||
@@ -54,8 +52,4 @@ const CourseGrade = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseGrade.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseGrade);
|
||||
export default CourseGrade;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CheckCircle, WarningFilled } from '@openedx/paragon/icons';
|
||||
import { breakpoints, Icon, useWindowSize } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||
import messages from '../messages';
|
||||
|
||||
const CourseGradeFooter = ({ intl, passingGrade }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const CourseGradeFooter = ({ passingGrade }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
courseGrade: {
|
||||
@@ -86,8 +84,7 @@ const CourseGradeFooter = ({ intl, passingGrade }) => {
|
||||
};
|
||||
|
||||
CourseGradeFooter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseGradeFooter);
|
||||
export default CourseGradeFooter;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Locked } from '@openedx/paragon/icons';
|
||||
import { Button, Icon } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import messages from '../messages';
|
||||
|
||||
const CourseGradeHeader = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const CourseGradeHeader = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
const {
|
||||
org,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
@@ -83,8 +80,4 @@ const CourseGradeHeader = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseGradeHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseGradeHeader);
|
||||
export default CourseGradeHeader;
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const CurrentGradeTooltip = ({ intl, tooltipClassName }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
courseGrade: {
|
||||
@@ -69,8 +65,7 @@ CurrentGradeTooltip.defaultProps = {
|
||||
};
|
||||
|
||||
CurrentGradeTooltip.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
tooltipClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(CurrentGradeTooltip);
|
||||
export default CurrentGradeTooltip;
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import CurrentGradeTooltip from './CurrentGradeTooltip';
|
||||
import PassingGradeTooltip from './PassingGradeTooltip';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const GradeBar = ({ intl, passingGrade }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const GradeBar = ({ passingGrade }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
courseGrade: {
|
||||
@@ -52,8 +48,7 @@ const GradeBar = ({ intl, passingGrade }) => {
|
||||
};
|
||||
|
||||
GradeBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GradeBar);
|
||||
export default GradeBar;
|
||||
|
||||
@@ -36,17 +36,17 @@
|
||||
}
|
||||
|
||||
#passing-grade-tooltip {
|
||||
background: $success-500;
|
||||
|
||||
.arrow::after {
|
||||
border-top-color: $success-500;
|
||||
}
|
||||
|
||||
background: $success-500;
|
||||
}
|
||||
|
||||
#non-passing-grade-tooltip {
|
||||
background: $accent-b;
|
||||
|
||||
.arrow::after {
|
||||
border-top-color: $accent-b;
|
||||
}
|
||||
|
||||
background: $accent-b;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
import {
|
||||
Icon, IconButton, OverlayTrigger, Popover,
|
||||
} from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const GradeRangeTooltip = ({ intl, iconButtonClassName, passingGrade }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const GradeRangeTooltip = ({ iconButtonClassName, passingGrade }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
gradesFeatureIsFullyLocked,
|
||||
@@ -80,8 +79,7 @@ GradeRangeTooltip.defaultProps = {
|
||||
|
||||
GradeRangeTooltip.propTypes = {
|
||||
iconButtonClassName: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GradeRangeTooltip);
|
||||
export default GradeRangeTooltip;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const PassingGradeTooltip = ({ intl, passingGrade, tooltipClassName }) => {
|
||||
const PassingGradeTooltip = ({ passingGrade, tooltipClassName }) => {
|
||||
const intl = useIntl();
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
||||
@@ -54,9 +52,8 @@ PassingGradeTooltip.defaultProps = {
|
||||
};
|
||||
|
||||
PassingGradeTooltip.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
tooltipClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(PassingGradeTooltip);
|
||||
export default PassingGradeTooltip;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Blocked } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Locked } from '@openedx/paragon/icons';
|
||||
import { Icon, Hyperlink } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import { showUngradedAssignments } from '../../utils';
|
||||
|
||||
import DetailedGradesTable from './DetailedGradesTable';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const DetailedGrades = ({ intl }) => {
|
||||
const DetailedGrades = () => {
|
||||
const intl = useIntl();
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const courseId = useContextId();
|
||||
const {
|
||||
org,
|
||||
tabs,
|
||||
@@ -28,6 +26,8 @@ const DetailedGrades = ({ intl }) => {
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const hasSectionScores = sectionScores.length > 0;
|
||||
const emptyTableMsg = showUngradedAssignments()
|
||||
? messages.detailedGradesEmpty : messages.detailedGradesEmptyOnlyGraded;
|
||||
|
||||
const logOutlineLinkClick = () => {
|
||||
sendTrackEvent('edx.ui.lms.course_progress.detailed_grades.course_outline_link.clicked', {
|
||||
@@ -54,35 +54,36 @@ const DetailedGrades = ({ intl }) => {
|
||||
|
||||
return (
|
||||
<section className="text-dark-700">
|
||||
<h3 className="h4 mb-3">{intl.formatMessage(messages.detailedGrades)}</h3>
|
||||
<h3 className="h4">{intl.formatMessage(messages.detailedGrades)}</h3>
|
||||
<ul className="micro mb-3 pl-3 text-gray-700">
|
||||
<li>
|
||||
<b>{intl.formatMessage(messages.practiceScoreLabel)} </b>
|
||||
{intl.formatMessage(messages.practiceScoreInfoText)}
|
||||
</li>
|
||||
<li>
|
||||
<b>{intl.formatMessage(messages.gradedScoreLabel)} </b>
|
||||
{intl.formatMessage(messages.gradedScoreInfoText)}
|
||||
</li>
|
||||
</ul>
|
||||
{gradesFeatureIsPartiallyLocked && (
|
||||
<div className="mb-3 small ml-0 d-inline">
|
||||
<Icon className="mr-1 mt-1 d-inline-flex" style={{ height: '1rem', width: '1rem' }} src={Blocked} data-testid="blocked-icon" />
|
||||
{intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation)}
|
||||
<Icon className="mr-1 mt-1 d-inline-flex" style={{ height: '1rem', width: '1rem' }} src={Locked} data-testid="locked-icon" />
|
||||
{intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation, { upgradeLink: '' })}
|
||||
</div>
|
||||
)}
|
||||
{hasSectionScores && (
|
||||
<DetailedGradesTable />
|
||||
)}
|
||||
{!hasSectionScores && (
|
||||
<p className="small">{intl.formatMessage(messages.detailedGradesEmpty)}</p>
|
||||
<p className="small">{intl.formatMessage(emptyTableMsg)}</p>
|
||||
)}
|
||||
{overviewTabUrl && (
|
||||
{overviewTabUrl && !showUngradedAssignments() && (
|
||||
<p className="x-small m-0">
|
||||
<FormattedMessage
|
||||
id="progress.ungradedAlert"
|
||||
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
||||
description="Text that precede link that redirect to course outline page"
|
||||
values={{ outlineLink }}
|
||||
/>
|
||||
{intl.formatMessage(messages.ungradedAlert, { outlineLink })}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
DetailedGrades.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DetailedGrades);
|
||||
export default DetailedGrades;
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import messages from '../messages';
|
||||
import SubsectionTitleCell from './SubsectionTitleCell';
|
||||
import { showUngradedAssignments } from '../../utils';
|
||||
|
||||
const DetailedGradesTable = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const DetailedGradesTable = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
sectionScores,
|
||||
@@ -24,9 +20,10 @@ const DetailedGradesTable = ({ intl }) => {
|
||||
sectionScores.map((chapter) => {
|
||||
const subsectionScores = chapter.subsections.filter(
|
||||
(subsection) => !!(
|
||||
subsection.hasGradedAssignment
|
||||
&& subsection.showGrades
|
||||
&& (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0)),
|
||||
(showUngradedAssignments() || subsection.hasGradedAssignment)
|
||||
&& subsection.showGrades
|
||||
&& (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0)
|
||||
),
|
||||
);
|
||||
|
||||
if (subsectionScores.length === 0) {
|
||||
@@ -66,8 +63,4 @@ const DetailedGradesTable = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
DetailedGradesTable.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DetailedGradesTable);
|
||||
export default DetailedGradesTable;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
|
||||
const ProblemScoreDrawer = ({ problemScores, subsection }) => {
|
||||
const intl = useIntl();
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
const scoreLabel = subsection.hasGradedAssignment ? messages.gradedScoreLabel : messages.practiceScoreLabel;
|
||||
|
||||
return (
|
||||
<span className="row w-100 m-0 x-small ml-4 pt-2 pl-1 text-gray-700 flex-nowrap">
|
||||
<span id="problem-score-label" className="col-auto p-0">{intl.formatMessage(messages.problemScoreLabel)}</span>
|
||||
<span id="problem-score-label" className="col-auto p-0">{intl.formatMessage(scoreLabel)}</span>
|
||||
<div className={classNames('col', 'p-0', { 'greyed-out': !subsection.learnerHasAccess })}>
|
||||
<ul className="list-unstyled row w-100 m-0" aria-labelledby="problem-score-label">
|
||||
{problemScores.map((problemScore, i) => (
|
||||
@@ -26,12 +27,14 @@ const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
|
||||
};
|
||||
|
||||
ProblemScoreDrawer.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
problemScores: PropTypes.arrayOf(PropTypes.shape({
|
||||
earned: PropTypes.number.isRequired,
|
||||
possible: PropTypes.number.isRequired,
|
||||
})).isRequired,
|
||||
subsection: PropTypes.shape({ learnerHasAccess: PropTypes.bool }).isRequired,
|
||||
subsection: PropTypes.shape({
|
||||
learnerHasAccess: PropTypes.bool,
|
||||
hasGradedAssignment: PropTypes.bool,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ProblemScoreDrawer);
|
||||
export default ProblemScoreDrawer;
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, Icon, Row } from '@openedx/paragon';
|
||||
import {
|
||||
ArrowDropDown, ArrowDropUp, Blocked, Info,
|
||||
ArrowDropDown,
|
||||
ArrowDropUp,
|
||||
Info,
|
||||
Locked,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import ProblemScoreDrawer from './ProblemScoreDrawer';
|
||||
|
||||
const SubsectionTitleCell = ({ intl, subsection }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const SubsectionTitleCell = ({ subsection }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
const {
|
||||
org,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
@@ -61,8 +62,8 @@ const SubsectionTitleCell = ({ intl, subsection }) => {
|
||||
aria-label={intl.formatMessage(messages.noAccessToSubsection, { displayName })}
|
||||
className="mr-1 mt-1 d-inline-flex"
|
||||
style={{ height: '1rem', width: '1rem' }}
|
||||
src={Blocked}
|
||||
data-testid="blocked-icon"
|
||||
src={Locked}
|
||||
data-testid="locked-icon"
|
||||
/>
|
||||
)}
|
||||
{url ? (
|
||||
@@ -102,7 +103,6 @@ const SubsectionTitleCell = ({ intl, subsection }) => {
|
||||
};
|
||||
|
||||
SubsectionTitleCell.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
subsection: PropTypes.shape({
|
||||
blockKey: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
@@ -119,4 +119,4 @@ SubsectionTitleCell.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SubsectionTitleCell);
|
||||
export default SubsectionTitleCell;
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Blocked } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Locked } from '@openedx/paragon/icons';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import messages from '../messages';
|
||||
|
||||
const AssignmentTypeCell = ({
|
||||
intl, assignmentType, footnoteMarker, footnoteId, locked,
|
||||
assignmentType, footnoteMarker, footnoteId, locked,
|
||||
}) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
gradesFeatureIsFullyLocked,
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const lockedIcon = locked ? <Icon id={`assignmentTypeBlockedIcon${assignmentType}`} aria-label={intl.formatMessage(messages.noAccessToAssignmentType, { assignmentType })} className="mr-1 mt-1 d-inline-flex" style={{ height: '1rem', width: '1rem' }} src={Blocked} data-testid="blocked-icon" /> : '';
|
||||
const lockedIcon = locked ? <Icon id={`assignmentTypeBlockedIcon${assignmentType}`} aria-label={intl.formatMessage(messages.noAccessToAssignmentType, { assignmentType })} className="mr-1 mt-1 d-inline-flex" style={{ height: '1rem', width: '1rem' }} src={Locked} data-testid="locked-icon" /> : '';
|
||||
|
||||
return (
|
||||
<div className="d-flex small">
|
||||
@@ -45,7 +43,6 @@ const AssignmentTypeCell = ({
|
||||
};
|
||||
|
||||
AssignmentTypeCell.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
assignmentType: PropTypes.string.isRequired,
|
||||
footnoteId: PropTypes.string,
|
||||
footnoteMarker: PropTypes.number,
|
||||
@@ -58,4 +55,4 @@ AssignmentTypeCell.defaultProps = {
|
||||
locked: false,
|
||||
};
|
||||
|
||||
export default injectIntl(AssignmentTypeCell);
|
||||
export default AssignmentTypeCell;
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
const DroppableAssignmentFootnote = ({ footnotes, intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const DroppableAssignmentFootnote = ({ footnotes }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
const {
|
||||
gradesFeatureIsFullyLocked,
|
||||
} = useModel('progress', courseId);
|
||||
@@ -21,14 +19,10 @@ const DroppableAssignmentFootnote = ({ footnotes, intl }) => {
|
||||
{footnotes.map((footnote, index) => (
|
||||
<li id={`${footnote.id}-footnote`} key={footnote.id} className="x-small mt-1">
|
||||
<sup>{index + 1}</sup>
|
||||
<FormattedMessage
|
||||
id="progress.footnotes.droppableAssignments"
|
||||
defaultMessage="The lowest {numDroppable, plural, one{# {assignmentType} score is} other{# {assignmentType} scores are}} dropped."
|
||||
values={{
|
||||
numDroppable: footnote.numDroppable,
|
||||
assignmentType: footnote.assignmentType,
|
||||
}}
|
||||
/>
|
||||
{intl.formatMessage(messages.droppableAssignmentsText, {
|
||||
numDroppable: footnote.numDroppable,
|
||||
assignmentType: footnote.assignmentType,
|
||||
})}
|
||||
<a className="sr-only" href={`#${footnote.id}-ref`} tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'}>
|
||||
{intl.formatMessage(messages.backToContent)}
|
||||
</a>
|
||||
@@ -45,7 +39,6 @@ DroppableAssignmentFootnote.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
numDroppable: PropTypes.number.isRequired,
|
||||
})).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DroppableAssignmentFootnote);
|
||||
export default DroppableAssignmentFootnote;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import GradeSummaryHeader from './GradeSummaryHeader';
|
||||
import GradeSummaryTable from './GradeSummaryTable';
|
||||
|
||||
const GradeSummary = () => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
gradingPolicy: {
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon, IconButton, OverlayTrigger, Popover,
|
||||
Hyperlink,
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { Blocked, InfoOutline } from '@openedx/paragon/icons';
|
||||
import { InfoOutline, Locked } from '@openedx/paragon/icons';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const GradeSummaryHeader = ({ allOfSomeAssignmentTypeIsLocked }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
const {
|
||||
verifiedMode,
|
||||
gradesFeatureIsFullyLocked,
|
||||
} = useModel('progress', courseId);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="row w-100 m-0 align-items-center">
|
||||
<h3 className="h4 mb-3 mr-1">{intl.formatMessage(messages.gradeSummary)}</h3>
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
placement="top"
|
||||
show={showTooltip}
|
||||
overlay={(
|
||||
<Popover>
|
||||
<Popover.Content className="small text-dark-700">
|
||||
<Stack gap={2} className="mb-3">
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<h3 className="h4 m-0">{intl.formatMessage(messages.gradeSummary)}</h3>
|
||||
<OverlayTrigger
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
{intl.formatMessage(messages.gradeSummaryTooltipBody)}
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => { setShowTooltip(!showTooltip); }}
|
||||
onBlur={() => { setShowTooltip(false); }}
|
||||
alt={intl.formatMessage(messages.gradeSummaryTooltipAlt)}
|
||||
src={InfoOutline}
|
||||
iconAs={Icon}
|
||||
className="mb-3"
|
||||
size="sm"
|
||||
disabled={gradesFeatureIsFullyLocked}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
alt={intl.formatMessage(messages.gradeSummaryTooltipAlt)}
|
||||
src={InfoOutline}
|
||||
size="sm"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</Stack>
|
||||
{!gradesFeatureIsFullyLocked && allOfSomeAssignmentTypeIsLocked && (
|
||||
<div className="mb-3 small ml-0 d-inline">
|
||||
<Icon className="mr-1 mt-1 d-inline-flex" style={{ height: '1rem', width: '1rem' }} src={Blocked} data-testid="blocked-icon" />
|
||||
{intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation)}
|
||||
</div>
|
||||
<Stack direction="horizontal" className="small" gap={2}>
|
||||
<Icon size="sm" src={Locked} data-testid="locked-icon" />
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
messages.gradeSummaryLimitedAccessExplanation,
|
||||
{
|
||||
upgradeLink: verifiedMode && (
|
||||
<Hyperlink destination={verifiedMode.upgradeUrl}>
|
||||
{intl.formatMessage(messages.courseGradePreviewUpgradeButton)}.
|
||||
</Hyperlink>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
GradeSummaryHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
allOfSomeAssignmentTypeIsLocked: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GradeSummaryHeader);
|
||||
export default GradeSummaryHeader;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import AssignmentTypeCell from './AssignmentTypeCell';
|
||||
@@ -14,10 +11,9 @@ import GradeSummaryTableFooter from './GradeSummaryTableFooter';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
gradingPolicy: {
|
||||
@@ -34,6 +30,14 @@ const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
return footnoteId.replace(/[^A-Za-z0-9.-_]+/g, '-');
|
||||
};
|
||||
|
||||
const getGradePercent = (grade) => {
|
||||
if (Number.isInteger(grade * 100)) {
|
||||
return (grade * 100).toFixed(0);
|
||||
}
|
||||
|
||||
return (grade * 100).toFixed(2);
|
||||
};
|
||||
|
||||
const hasNoAccessToAssignmentsOfType = (assignmentType) => {
|
||||
const subsectionAssignmentsOfType = sectionScores.map((chapter) => chapter.subsections.filter((subsection) => (
|
||||
subsection.assignmentType === assignmentType && subsection.hasGradedAssignment
|
||||
@@ -52,31 +56,37 @@ const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
};
|
||||
|
||||
const gradeSummaryData = assignmentPolicies.map((assignment) => {
|
||||
const {
|
||||
averageGrade,
|
||||
numDroppable,
|
||||
type: assignmentType,
|
||||
weight,
|
||||
weightedGrade,
|
||||
} = assignment;
|
||||
let footnoteId = '';
|
||||
let footnoteMarker;
|
||||
|
||||
if (assignment.numDroppable > 0) {
|
||||
if (numDroppable > 0) {
|
||||
footnoteId = getFootnoteId(assignment);
|
||||
footnotes.push({
|
||||
id: footnoteId,
|
||||
numDroppable: assignment.numDroppable,
|
||||
assignmentType: assignment.type,
|
||||
numDroppable,
|
||||
assignmentType,
|
||||
});
|
||||
|
||||
footnoteMarker = footnotes.length;
|
||||
}
|
||||
|
||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type);
|
||||
|
||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignmentType);
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return {
|
||||
type: {
|
||||
footnoteId, footnoteMarker, type: assignment.type, locked,
|
||||
footnoteId, footnoteMarker, type: assignmentType, locked,
|
||||
},
|
||||
weight: { weight: `${(assignment.weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
weight: { weight: `${(weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
grade: { grade: `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
};
|
||||
});
|
||||
const getAssignmentTypeCell = (value) => (
|
||||
@@ -137,8 +147,7 @@ const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
};
|
||||
|
||||
GradeSummaryTable.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
setAllOfSomeAssignmentTypeIsLocked: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GradeSummaryTable);
|
||||
export default GradeSummaryTable;
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@openedx/paragon';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
DataTable,
|
||||
DataTableContext,
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import messages from '../messages';
|
||||
|
||||
const GradeSummaryTableFooter = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const GradeSummaryTableFooter = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { data } = useContext(DataTableContext);
|
||||
|
||||
const rawGrade = data.reduce(
|
||||
(grade, currentValue) => {
|
||||
const { weightedGrade } = currentValue.weightedGrade;
|
||||
const percent = weightedGrade.replace(/%/g, '').trim();
|
||||
return grade + parseFloat(percent);
|
||||
},
|
||||
0,
|
||||
).toFixed(2);
|
||||
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
courseGrade: {
|
||||
@@ -29,15 +46,33 @@ const GradeSummaryTableFooter = ({ intl }) => {
|
||||
return (
|
||||
<DataTable.TableFooter className={`border-top border-primary ${bgColor}`}>
|
||||
<div className="row w-100 m-0">
|
||||
<div id="weighted-grade-summary" className="col-8 p-0 small">{intl.formatMessage(messages.weightedGradeSummary)}</div>
|
||||
<div id="weighted-grade-summary" className="col-8 p-0 small">
|
||||
<Stack gap={2} direction="horizontal">
|
||||
{intl.formatMessage(messages.weightedGradeSummary)}
|
||||
<OverlayTrigger
|
||||
trigger="hover"
|
||||
placement="bottom"
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
{intl.formatMessage(
|
||||
messages.weightedGradeSummaryTooltip,
|
||||
{ roundedGrade: totalGrade, rawGrade },
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
src={InfoOutline}
|
||||
size="sm"
|
||||
alt={intl.formatMessage(messages.gradeSummaryTooltipAlt)}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</Stack>
|
||||
</div>
|
||||
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}{isLocaleRtl && '\u200f'}%</div>
|
||||
</div>
|
||||
</DataTable.TableFooter>
|
||||
);
|
||||
};
|
||||
|
||||
GradeSummaryTableFooter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GradeSummaryTableFooter);
|
||||
export default GradeSummaryTableFooter;
|
||||
|
||||
@@ -78,7 +78,7 @@ const messages = defineMessages({
|
||||
},
|
||||
courseOutline: {
|
||||
id: 'progress.courseOutline',
|
||||
defaultMessage: 'Course Outline',
|
||||
defaultMessage: 'Course outline',
|
||||
description: 'Anchor text for link that redirects to (course outline) tab',
|
||||
},
|
||||
currentGradeLabel: {
|
||||
@@ -91,9 +91,14 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Detailed grades',
|
||||
description: 'Headline for the (detailed grade) section in the progress tab',
|
||||
},
|
||||
detailedGradesEmpty: {
|
||||
detailedGradesEmptyOnlyGraded: {
|
||||
id: 'progress.detailedGrades.emptyTable',
|
||||
defaultMessage: 'You currently have no graded problem scores.',
|
||||
description: 'It indicate that there are no graded problem or assignments to be scored',
|
||||
},
|
||||
detailedGradesEmpty: {
|
||||
id: 'progress.detailedGrades.including-ungraded.emptyTable',
|
||||
defaultMessage: 'You currently have no graded or ungraded problem scores.',
|
||||
description: 'It indicate that there are no problem or assignments to be scored',
|
||||
},
|
||||
footnotesTitle: {
|
||||
@@ -128,7 +133,7 @@ const messages = defineMessages({
|
||||
},
|
||||
gradeSummaryLimitedAccessExplanation: {
|
||||
id: 'progress.gradeSummary.limitedAccessExplanation',
|
||||
defaultMessage: 'You have limited access to graded assignments as part of the audit track in this course.',
|
||||
defaultMessage: 'You have limited access to graded assignments as part of the audit track in this course. {upgradeLink}',
|
||||
description: 'Text shown when learner has limited access to grade feature',
|
||||
},
|
||||
gradeSummaryTooltipAlt: {
|
||||
@@ -158,11 +163,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Passing grade',
|
||||
description: 'Label for mark on the (grade bar) chart which indicate the poisition of passing grade on the bar',
|
||||
},
|
||||
problemScoreLabel: {
|
||||
gradedScoreLabel: {
|
||||
id: 'progress.detailedGrades.problemScore.label',
|
||||
defaultMessage: 'Problem Scores:',
|
||||
defaultMessage: 'Graded Scores:',
|
||||
description: 'Label text which precedes detailed view of all scores per assignment',
|
||||
},
|
||||
practiceScoreLabel: {
|
||||
id: 'progress.detailedGrades.practice.problemScore.label',
|
||||
defaultMessage: 'Practice Scores:',
|
||||
description: 'Label text which precedes detailed view of all ungraded problem scores per assignment',
|
||||
},
|
||||
problemScoreToggleAltText: {
|
||||
id: 'progress.detailedGrades.problemScore.toggleButton',
|
||||
defaultMessage: 'Toggle individual problem scores for {subsectionTitle}',
|
||||
@@ -193,7 +203,31 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Your current weighted grade summary',
|
||||
description: 'It the text precede the sum of weighted grades of all the assignment',
|
||||
},
|
||||
|
||||
weightedGradeSummaryTooltip: {
|
||||
id: 'progress.weightedGradeSummary',
|
||||
defaultMessage: 'Your raw weighted grade summary is {rawGrade} and rounds to {roundedGrade}.',
|
||||
description: 'Tooltip content that explains the rounding of the summary versus individual assignments',
|
||||
},
|
||||
practiceScoreInfoText: {
|
||||
id: 'progress.detailedGrades.practice-label.info.text',
|
||||
defaultMessage: 'Scores from non-graded activities meant for practice and self-assessment.',
|
||||
description: 'Information text about non-graded practice score label',
|
||||
},
|
||||
gradedScoreInfoText: {
|
||||
id: 'progress.detailedGrades.problem-label.info.text',
|
||||
defaultMessage: 'Scores from activities that contribute to your final grade.',
|
||||
description: 'Information text about graded problem score label',
|
||||
},
|
||||
ungradedAlert: {
|
||||
id: 'progress.ungradedAlert',
|
||||
defaultMessage: 'For progress on ungraded aspects of the course, view your {outlineLink}.',
|
||||
description: 'Text that precede link that redirect to course outline page',
|
||||
},
|
||||
droppableAssignmentsText: {
|
||||
id: 'progress.footnotes.droppableAssignments',
|
||||
defaultMessage: 'The lowest {numDroppable, plural, one{# {assignmentType} score is} other{# {assignmentType} scores are}} dropped.',
|
||||
description: 'Footnote text stating how many assignments are dropped',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { useContextId } from '../../../data/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const RelatedLinks = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const RelatedLinks = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
const {
|
||||
org,
|
||||
tabs,
|
||||
@@ -58,8 +55,4 @@ const RelatedLinks = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
RelatedLinks.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RelatedLinks);
|
||||
export default RelatedLinks;
|
||||
|
||||
@@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||
},
|
||||
outlineCardLink: {
|
||||
id: 'progress.relatedLinks.outlineCard.link',
|
||||
defaultMessage: 'Course Outline',
|
||||
defaultMessage: 'Course outline',
|
||||
description: 'Anchor text for link that redirects to course outline tab',
|
||||
},
|
||||
relatedLinks: {
|
||||
7
src/course-home/progress-tab/utils.ts
Normal file
7
src/course-home/progress-tab/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export const showUngradedAssignments = () => (
|
||||
getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === 'true'
|
||||
|| getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === true
|
||||
);
|
||||
@@ -73,13 +73,13 @@ describe('Course Tabs Navigation', () => {
|
||||
it('should NOT render CoursewareSearch if the flag is off', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByTestId('courseware-search-section')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('courseware-search-dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render CoursewareSearch if the flag is on', () => {
|
||||
useCoursewareSearchState.mockImplementation(() => ({ show: true }));
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByTestId('courseware-search-section')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('courseware-search-dialog')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,62 +19,50 @@ import { handleNextSectionCelebration } from './course/celebration';
|
||||
import withParamsAndNavigation from './utils';
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId, navigate) => {
|
||||
if (courseStatus === 'loaded' && !sequenceId) {
|
||||
// Note that getResumeBlock is just an API call, not a redux thunk.
|
||||
getResumeBlock(courseId).then((data) => {
|
||||
// This is a replace because we don't want this change saved in the browser's history.
|
||||
if (data.sectionId && data.unitId) {
|
||||
navigate(`/course/${courseId}/${data.sectionId}/${data.unitId}`, { replace: true });
|
||||
} else if (firstSequenceId) {
|
||||
navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
|
||||
navigate(`/course/${courseId}/${unitId}`, { replace: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
|
||||
// If the section is non-empty, redirect to its first sequence.
|
||||
if (section.sequenceIds && section.sequenceIds[0]) {
|
||||
navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true });
|
||||
// Otherwise, just go to the course root, letting the resume redirect take care of things.
|
||||
} else {
|
||||
navigate(`/course/${courseId}`, { replace: true });
|
||||
export const checkResumeRedirect = memoize(
|
||||
(courseStatus, courseId, sequenceId, firstSequenceId, navigate, isPreview) => {
|
||||
if (courseStatus === 'loaded' && !sequenceId) {
|
||||
// Note that getResumeBlock is just an API call, not a redux thunk.
|
||||
getResumeBlock(courseId).then((data) => {
|
||||
// This is a replace because we don't want this change saved in the browser's history.
|
||||
if (data.sectionId && data.unitId) {
|
||||
const baseUrl = `/course/${courseId}/${data.sectionId}`;
|
||||
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
navigate(`${sequenceUrl}/${data.unitId}`, { replace: true });
|
||||
} else if (firstSequenceId) {
|
||||
navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true });
|
||||
}
|
||||
}, () => {});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
export const checkSectionUnitToUnitRedirect = memoize((
|
||||
courseStatus,
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
section,
|
||||
unitId,
|
||||
navigate,
|
||||
isPreview,
|
||||
) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
|
||||
const baseUrl = `/course/${courseId}`;
|
||||
const courseUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
navigate(`${courseUrl}/${unitId}`, { replace: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
const checkUnitToSequenceUnitRedirect = memoize(
|
||||
(courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId, navigate) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
||||
if (sequenceMightBeUnit) {
|
||||
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
|
||||
// we need to look up the correct parent sequence for it, and redirect there.
|
||||
const unitId = sequenceId; // just for clarity during the rest of this method
|
||||
getSequenceForUnitDeprecated(courseId, unitId).then(
|
||||
parentId => {
|
||||
if (parentId) {
|
||||
navigate(`/course/${courseId}/${parentId}/${unitId}`, { replace: true });
|
||||
} else {
|
||||
navigate(`/course/${courseId}`, { replace: true });
|
||||
}
|
||||
},
|
||||
() => { // error case
|
||||
navigate(`/course/${courseId}`, { replace: true });
|
||||
},
|
||||
);
|
||||
export const checkSectionToSequenceRedirect = memoize(
|
||||
(courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
|
||||
// If the section is non-empty, redirect to its first sequence.
|
||||
if (section.sequenceIds && section.sequenceIds[0]) {
|
||||
navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true });
|
||||
// Otherwise, just go to the course root, letting the resume redirect take care of things.
|
||||
} else {
|
||||
// Invalid sequence that isn't a unit either. Redirect up to main course.
|
||||
navigate(`/course/${courseId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
@@ -82,41 +70,80 @@ const checkUnitToSequenceUnitRedirect = memoize(
|
||||
);
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId, navigate) => {
|
||||
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
|
||||
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
|
||||
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
|
||||
// This is a replace because we don't want this change saved in the browser's history.
|
||||
navigate(`/course/${courseId}/${sequence.id}/${nextUnitId}`, { replace: true });
|
||||
export const checkUnitToSequenceUnitRedirect = memoize((
|
||||
courseStatus,
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
sequenceMightBeUnit,
|
||||
sequenceId,
|
||||
section,
|
||||
routeUnitId,
|
||||
navigate,
|
||||
isPreview,
|
||||
) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
||||
if (sequenceMightBeUnit) {
|
||||
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
|
||||
// we need to look up the correct parent sequence for it, and redirect there.
|
||||
const unitId = sequenceId; // just for clarity during the rest of this method
|
||||
getSequenceForUnitDeprecated(courseId, unitId).then(
|
||||
parentId => {
|
||||
if (parentId) {
|
||||
const baseUrl = `/course/${courseId}/${parentId}`;
|
||||
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
navigate(`${sequenceUrl}/${unitId}`, { replace: true });
|
||||
} else {
|
||||
navigate(`/course/${courseId}`, { replace: true });
|
||||
}
|
||||
},
|
||||
() => { // error case
|
||||
navigate(`/course/${courseId}`, { replace: true });
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Invalid sequence that isn't a unit either. Redirect up to main course.
|
||||
navigate(`/course/${courseId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize(
|
||||
(courseId, sequenceStatus, sequence, unitId, navigate) => {
|
||||
export const checkSequenceToSequenceUnitRedirect = memoize(
|
||||
(courseId, sequenceStatus, sequence, unitId, navigate, isPreview) => {
|
||||
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
|
||||
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
|
||||
const baseUrl = `/course/${courseId}/${sequence.id}`;
|
||||
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
|
||||
// This is a replace because we don't want this change saved in the browser's history.
|
||||
navigate(`${sequenceUrl}/${nextUnitId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||
export const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize(
|
||||
(courseId, sequenceStatus, sequence, unitId, navigate, isPreview) => {
|
||||
if (sequenceStatus !== 'loaded' || !sequence.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = `/course/${courseId}/${sequence.id}`;
|
||||
const hasUnits = sequence.unitIds?.length > 0;
|
||||
|
||||
if (unitId === 'first') {
|
||||
if (hasUnits) {
|
||||
if (hasUnits) {
|
||||
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
if (unitId === 'first') {
|
||||
const firstUnitId = sequence.unitIds[0];
|
||||
navigate(`/course/${courseId}/${sequence.id}/${firstUnitId}`, { replace: true });
|
||||
} else {
|
||||
// No units... go to general sequence page
|
||||
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
|
||||
}
|
||||
} else if (unitId === 'last') {
|
||||
if (hasUnits) {
|
||||
navigate(`${sequenceUrl}/${firstUnitId}`, { replace: true });
|
||||
} else if (unitId === 'last') {
|
||||
const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1];
|
||||
navigate(`/course/${courseId}/${sequence.id}/${lastUnitId}`, { replace: true });
|
||||
} else {
|
||||
// No units... go to general sequence page
|
||||
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
|
||||
navigate(`${sequenceUrl}/${lastUnitId}`, { replace: true });
|
||||
}
|
||||
} else {
|
||||
// No units... go to general sequence page
|
||||
navigate(baseUrl, { replace: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -141,7 +168,7 @@ class CoursewareContainer extends Component {
|
||||
|
||||
checkFetchSequence = memoize((sequenceId) => {
|
||||
if (sequenceId) {
|
||||
this.props.fetchSequence(sequenceId);
|
||||
this.props.fetchSequence(sequenceId, this.props.isPreview);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -169,6 +196,7 @@ class CoursewareContainer extends Component {
|
||||
routeSequenceId,
|
||||
routeUnitId,
|
||||
navigate,
|
||||
isPreview,
|
||||
} = this.props;
|
||||
|
||||
// Load data whenever the course or sequence ID changes.
|
||||
@@ -197,7 +225,7 @@ class CoursewareContainer extends Component {
|
||||
// Check resume redirect:
|
||||
// /course/:courseId -> /course/:courseId/:sequenceId/:unitId
|
||||
// based on sequence/unit where user was last active.
|
||||
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate);
|
||||
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate, isPreview);
|
||||
|
||||
// Check section-unit to unit redirect:
|
||||
// /course/:courseId/:sectionId/:unitId -> /course/:courseId/:unitId
|
||||
@@ -210,33 +238,69 @@ class CoursewareContainer extends Component {
|
||||
// otherwise, we could get stuck in a redirect loop, since a sequence that failed to load
|
||||
// would endlessly redirect to itself through `checkSectionUnitToUnitRedirect`
|
||||
// and `checkUnitToSequenceUnitRedirect`.
|
||||
checkSectionUnitToUnitRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
|
||||
checkSectionUnitToUnitRedirect(
|
||||
courseStatus,
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
sectionViaSequenceId,
|
||||
routeUnitId,
|
||||
navigate,
|
||||
isPreview,
|
||||
);
|
||||
|
||||
// Check section to sequence redirect:
|
||||
// /course/:courseId/:sectionId -> /course/:courseId/:sequenceId
|
||||
// by redirecting to the first sequence within the section.
|
||||
checkSectionToSequenceRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
|
||||
checkSectionToSequenceRedirect(
|
||||
courseStatus,
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
sectionViaSequenceId,
|
||||
routeUnitId,
|
||||
navigate,
|
||||
);
|
||||
|
||||
// Check unit to sequence-unit redirect:
|
||||
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
|
||||
// by filling in the ID of the parent sequence of :unitId.
|
||||
checkUnitToSequenceUnitRedirect((
|
||||
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit,
|
||||
sequenceId, sectionViaSequenceId, routeUnitId, navigate
|
||||
));
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
courseStatus,
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
sequenceMightBeUnit,
|
||||
sequenceId,
|
||||
sectionViaSequenceId,
|
||||
routeUnitId,
|
||||
navigate,
|
||||
isPreview,
|
||||
);
|
||||
|
||||
// Check sequence to sequence-unit redirect:
|
||||
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
|
||||
// by filling in the ID the most-recently-active unit in the sequence, OR
|
||||
// the ID of the first unit the sequence if none is active.
|
||||
checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
|
||||
checkSequenceToSequenceUnitRedirect(
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
sequence,
|
||||
routeUnitId,
|
||||
navigate,
|
||||
isPreview,
|
||||
);
|
||||
|
||||
// Check sequence-unit marker to sequence-unit redirect:
|
||||
// /course/:courseId/:sequenceId/first -> /course/:courseId/:sequenceId/:unitId
|
||||
// /course/:courseId/:sequenceId/last -> /course/:courseId/:sequenceId/:unitId
|
||||
// by filling in the ID the first or last unit in the sequence.
|
||||
// "Sequence unit marker" is an invented term used only in this component.
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
courseId,
|
||||
sequenceStatus,
|
||||
sequence,
|
||||
routeUnitId,
|
||||
navigate,
|
||||
isPreview,
|
||||
);
|
||||
}
|
||||
|
||||
handleUnitNavigationClick = () => {
|
||||
@@ -334,6 +398,7 @@ CoursewareContainer.propTypes = {
|
||||
fetchCourse: PropTypes.func.isRequired,
|
||||
fetchSequence: PropTypes.func.isRequired,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
isPreview: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
CoursewareContainer.defaultProps = {
|
||||
|
||||
@@ -16,11 +16,19 @@ import tabMessages from '../tab-page/messages';
|
||||
import { initializeMockApp, waitFor } from '../setupTest';
|
||||
import { DECODE_ROUTES } from '../constants';
|
||||
|
||||
import CoursewareContainer from './CoursewareContainer';
|
||||
import CoursewareContainer, {
|
||||
checkResumeRedirect,
|
||||
checkSectionToSequenceRedirect,
|
||||
checkSectionUnitToUnitRedirect,
|
||||
checkSequenceToSequenceUnitRedirect,
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect,
|
||||
checkUnitToSequenceUnitRedirect,
|
||||
} from './CoursewareContainer';
|
||||
import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory';
|
||||
import initializeStore from '../store';
|
||||
import { appendBrowserTimezoneToUrl } from '../utils';
|
||||
import { buildOutlineFromBlocks } from './data/__factories__/learningSequencesOutline.factory';
|
||||
import { getSequenceForUnitDeprecatedUrl } from './data/api';
|
||||
|
||||
// NOTE: Because the unit creates an iframe, we choose to mock it out as its rendering isn't
|
||||
// pertinent to this test. Instead, we render a simple div that displays the properties we expect
|
||||
@@ -525,3 +533,838 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Course redirect functions', () => {
|
||||
let navigate;
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
navigate = jest.fn();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
describe('isPreview equals true', () => {
|
||||
describe('checkSequenceUnitMarkerToSequenceUnitRedirect', () => {
|
||||
it('return when sequence is not loaded', () => {
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loading',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1'] },
|
||||
'first',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('return when sequence id is null', () => {
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: null, unitIds: ['unit_1'] },
|
||||
'first',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate with first unit id', () => {
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||
'first',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/preview/course/courseId/sequence_1/unit_1';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
|
||||
it('calls navigate with last unit id', () => {
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||
'last',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/preview/course/courseId/sequence_1/unit_2';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSequenceToSequenceUnitRedirect', () => {
|
||||
it('calls navigate with next unit id', () => {
|
||||
checkSequenceToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/preview/course/courseId/sequence_1/unit_1';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
|
||||
it('returns when sequence status is loading', () => {
|
||||
checkSequenceToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loading',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when sequence id is null', () => {
|
||||
checkSequenceToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loading',
|
||||
{ unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when unit id is defined', () => {
|
||||
checkSequenceToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||
'unit_2',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when unit ids are undefiend', () => {
|
||||
checkSequenceToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', activeUnitIndex: 0 },
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkUnitToSequenceUnitRedirect', () => {
|
||||
const { href: apiUrl } = getSequenceForUnitDeprecatedUrl('courseId');
|
||||
|
||||
it('calls navigate with parentId and sequenceId', () => {
|
||||
const getSequenceForUnitDeprecated = jest.fn();
|
||||
axiosMock.onGet(apiUrl).reply(200, {
|
||||
blocks: [{
|
||||
id: 'sequence_1',
|
||||
type: 'sequential',
|
||||
children: ['unit_1'],
|
||||
}],
|
||||
});
|
||||
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1';
|
||||
|
||||
waitFor(() => {
|
||||
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigate to course page when getSequenceForUnitDeprecated errors', () => {
|
||||
const getSequenceForUnitDeprecated = jest.fn();
|
||||
axiosMock.onGet(apiUrl).reply(404);
|
||||
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId';
|
||||
|
||||
waitFor(() => {
|
||||
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigate to course page when no parent id is returned', () => {
|
||||
const getSequenceForUnitDeprecated = jest.fn();
|
||||
axiosMock.onGet(apiUrl).reply(200, {
|
||||
blocks: [{
|
||||
id: 'sequence_1',
|
||||
type: 'sequential',
|
||||
children: ['block_1'],
|
||||
}],
|
||||
});
|
||||
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId';
|
||||
|
||||
waitFor(() => {
|
||||
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigate to course page when sequnce is not unit', () => {
|
||||
const getSequenceForUnitDeprecated = jest.fn();
|
||||
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
false,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId';
|
||||
|
||||
waitFor(() => {
|
||||
expect(getSequenceForUnitDeprecated).not.toHaveBeenCalled();
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('returns when course status is loading', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loading',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when sequence status is not failed', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when section is defined', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_1',
|
||||
true,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when routeUnitId is defined', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
'unit_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSectionUnitToUnitRedirect', () => {
|
||||
it('calls navigate with unitId', () => {
|
||||
checkSectionUnitToUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_2',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/preview/course/courseId/unit_2';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
|
||||
it('returns when course status is loading', () => {
|
||||
checkSectionUnitToUnitRedirect(
|
||||
'loading',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_2',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when sequence status is loading', () => {
|
||||
checkSectionUnitToUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_2',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when section is null', () => {
|
||||
checkSectionUnitToUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
null,
|
||||
'unit_2',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when unitId is null', () => {
|
||||
checkSectionUnitToUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkResumeRedirect', () => {
|
||||
it('calls navigate with unitId', () => {
|
||||
axiosMock.onGet(
|
||||
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||
).reply(200, {
|
||||
section_id: 'section_1',
|
||||
unitId: 'unit_1',
|
||||
});
|
||||
checkResumeRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
null,
|
||||
'sequence_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/preview/course/courseId/section_1/unit_1';
|
||||
|
||||
waitFor(() => {
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigate with firstSequenceId', () => {
|
||||
axiosMock.onGet(
|
||||
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||
).reply(200, {
|
||||
section_id: 'section_1',
|
||||
first_sequence_id: 'sequence_1',
|
||||
});
|
||||
checkResumeRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
null,
|
||||
'sequence_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1';
|
||||
|
||||
waitFor(() => {
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('returns after calling getResumeBlock', () => {
|
||||
const getResumeBlock = jest.fn();
|
||||
axiosMock.onGet(
|
||||
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||
).reply(200, {
|
||||
course_id: 'courseId',
|
||||
});
|
||||
checkResumeRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
null,
|
||||
'sequence_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
waitFor(() => {
|
||||
expect(getResumeBlock).toHaveBeenCalled();
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns when course status is loading', () => {
|
||||
checkResumeRedirect(
|
||||
'loading',
|
||||
'courseId',
|
||||
null,
|
||||
'sequence_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when sequenceId is defined', () => {
|
||||
checkResumeRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'sequence_3',
|
||||
'sequence_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when getResumeBlock throws error', () => {
|
||||
const getResumeBlock = jest.fn();
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`).reply(404);
|
||||
checkResumeRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
null,
|
||||
'sequence_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
waitFor(() => {
|
||||
expect(getResumeBlock).toHaveBeenCalled();
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPreview equals false', () => {
|
||||
describe('checkSectionToSequenceRedirect', () => {
|
||||
it('calls navigate with section based sequence id', () => {
|
||||
checkSectionToSequenceRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
{ sequenceIds: ['sequence_1'] },
|
||||
null,
|
||||
navigate,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
|
||||
it('calls navigate with course id only', () => {
|
||||
checkSectionToSequenceRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
{ sequenceIds: [] },
|
||||
null,
|
||||
navigate,
|
||||
);
|
||||
const expectedUrl = '/course/courseId';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
|
||||
it('returns when course status is loading', () => {
|
||||
checkSectionToSequenceRedirect(
|
||||
'loading',
|
||||
'courseId',
|
||||
'failed',
|
||||
{ sequenceIds: [] },
|
||||
null,
|
||||
navigate,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when sequence status is not failed', () => {
|
||||
checkSectionToSequenceRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loading',
|
||||
{ sequenceIds: [] },
|
||||
null,
|
||||
navigate,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when section is not defined', () => {
|
||||
checkSectionToSequenceRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
null,
|
||||
null,
|
||||
navigate,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when unitId is defined', () => {
|
||||
checkSectionToSequenceRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
null,
|
||||
'unit_1',
|
||||
navigate,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkResumeRedirect', () => {
|
||||
it('calls navigate with unitId', () => {
|
||||
axiosMock.onGet(
|
||||
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||
).reply(200, {
|
||||
section_id: 'section_1',
|
||||
unitId: 'unit_1',
|
||||
});
|
||||
checkResumeRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
null,
|
||||
'sequence_1',
|
||||
navigate,
|
||||
false,
|
||||
);
|
||||
const expectedUrl = '/preview/course/courseId/section_1/unit_1';
|
||||
|
||||
waitFor(() => {
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigate with firstSequenceId', () => {
|
||||
axiosMock.onGet(
|
||||
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||
).reply(200, {
|
||||
section_id: 'section_1',
|
||||
first_sequence_id: 'sequence_1',
|
||||
});
|
||||
checkResumeRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
null,
|
||||
'sequence_1',
|
||||
navigate,
|
||||
false,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1';
|
||||
|
||||
waitFor(() => {
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSequenceUnitMarkerToSequenceUnitRedirect', () => {
|
||||
it('calls navigate with first unit id', () => {
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||
'first',
|
||||
navigate,
|
||||
false,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1/unit_1';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
|
||||
it('calls navigate with base url when no unit id', () => {
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: [] },
|
||||
'first',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
|
||||
it('calls navigate with last unit id', () => {
|
||||
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||
'last',
|
||||
navigate,
|
||||
false,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1/unit_2';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSequenceToSequenceUnitRedirect', () => {
|
||||
it('calls navigate with next unit id', () => {
|
||||
checkSequenceToSequenceUnitRedirect(
|
||||
'courseId',
|
||||
'loaded',
|
||||
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||
null,
|
||||
navigate,
|
||||
false,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1/unit_1';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkUnitToSequenceUnitRedirect', () => {
|
||||
const apiUrl = getSequenceForUnitDeprecatedUrl('courseId');
|
||||
|
||||
it('calls navigate with parentId and sequenceId', () => {
|
||||
axiosMock.onGet(apiUrl).reply(200, {
|
||||
parent: { id: 'sequence_1' },
|
||||
});
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/sequence_1';
|
||||
|
||||
waitFor(() => {
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigate to course page when getSequenceForUnitDeprecated errors', () => {
|
||||
const getSequenceForUnitDeprecated = jest.fn();
|
||||
axiosMock.onGet(apiUrl).reply(404);
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId';
|
||||
|
||||
waitFor(() => {
|
||||
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigate to course page when no parent id is returned', () => {
|
||||
const getSequenceForUnitDeprecated = jest.fn();
|
||||
axiosMock.onGet(apiUrl).reply(200, {
|
||||
parent: { children: ['block_1'] },
|
||||
});
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
const expectedUrl = '/course/courseId';
|
||||
|
||||
waitFor(() => {
|
||||
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('returns when course status is loading', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loading',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when sequence status is not failed', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when section is defined', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_1',
|
||||
true,
|
||||
null,
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns when routeUnitId is defined', () => {
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'loaded',
|
||||
true,
|
||||
'unit_1',
|
||||
false,
|
||||
'unit_1',
|
||||
navigate,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSectionUnitToUnitRedirect', () => {
|
||||
it('calls navigate with unitId', () => {
|
||||
checkSectionUnitToUnitRedirect(
|
||||
'loaded',
|
||||
'courseId',
|
||||
'failed',
|
||||
true,
|
||||
'unit_2',
|
||||
navigate,
|
||||
false,
|
||||
);
|
||||
const expectedUrl = '/course/courseId/unit_2';
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,21 +3,21 @@ import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
|
||||
import { AlertList } from '@src/generic/user-messages';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { getCoursewareOutlineSidebarSettings } from '../data/selectors';
|
||||
import { Trigger as CourseOutlineTrigger } from './sidebar/sidebars/course-outline';
|
||||
import Chat from './chat/Chat';
|
||||
import SidebarProvider from './sidebar/SidebarContextProvider';
|
||||
import SidebarTriggers from './sidebar/SidebarTriggers';
|
||||
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
|
||||
import NewSidebarTriggers from './new-sidebar/SidebarTriggers';
|
||||
import { NotificationsDiscussionsSidebarTriggerSlot } from '../../plugin-slots/NotificationsDiscussionsSidebarTriggerSlot';
|
||||
import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
import ContentTools from './content-tools';
|
||||
import Sequence from './sequence';
|
||||
import { CourseOutlineMobileSidebarTriggerSlot } from '../../plugin-slots/CourseOutlineMobileSidebarTriggerSlot';
|
||||
import { CourseBreadcrumbsSlot } from '../../plugin-slots/CourseBreadcrumbsSlot';
|
||||
|
||||
const Course = ({
|
||||
courseId,
|
||||
@@ -33,11 +33,19 @@ const Course = ({
|
||||
celebrations,
|
||||
isStaff,
|
||||
isNewDiscussionSidebarViewEnabled,
|
||||
originalUserIsStaff,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
||||
const { enableNavigationSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
const navigationDisabled = enableNavigationSidebar || (sequence?.navigationDisabled ?? false);
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (!originalUserIsStaff && pathname.startsWith('/preview')) {
|
||||
const courseUrl = pathname.replace('/preview', '');
|
||||
navigate(courseUrl, { replace: true });
|
||||
}
|
||||
|
||||
const pageTitleBreadCrumbs = [
|
||||
sequence,
|
||||
@@ -78,7 +86,7 @@ const Course = ({
|
||||
<div className="position-relative d-flex align-items-xl-center mb-4 mt-1 flex-column flex-xl-row">
|
||||
{navigationDisabled || (
|
||||
<>
|
||||
<CourseBreadcrumbs
|
||||
<CourseBreadcrumbsSlot
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
@@ -100,8 +108,8 @@ const Course = ({
|
||||
</>
|
||||
)}
|
||||
<div className="w-100 d-flex align-items-center">
|
||||
<CourseOutlineTrigger isMobileView />
|
||||
{isNewDiscussionSidebarViewEnabled ? <NewSidebarTriggers /> : <SidebarTriggers /> }
|
||||
<CourseOutlineMobileSidebarTriggerSlot />
|
||||
<NotificationsDiscussionsSidebarTriggerSlot courseId={courseId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration;
|
||||
|
||||
describe('Course', () => {
|
||||
let store;
|
||||
let getItemSpy;
|
||||
let setItemSpy;
|
||||
const mockData = {
|
||||
nextSequenceHandler: () => {},
|
||||
previousSequenceHandler: () => {},
|
||||
@@ -52,30 +50,27 @@ describe('Course', () => {
|
||||
global.innerWidth = breakpoints.extraLarge.minWidth;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getItemSpy.mockRestore();
|
||||
setItemSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('loads learning sequence', async () => {
|
||||
render(<Course {...mockData} />, { wrapWithRouter: true });
|
||||
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
expect(screen.findByText('Loading learning sequence...')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Learn About Verified Certificates' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Learn About Verified Certificates' })).not.toBeInTheDocument();
|
||||
|
||||
loadUnit();
|
||||
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
|
||||
loadUnit();
|
||||
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
|
||||
|
||||
const { models } = store.getState();
|
||||
const sequence = models.sequences[mockData.sequenceId];
|
||||
const section = models.sections[sequence.sectionId];
|
||||
const course = models.coursewareMeta[mockData.courseId];
|
||||
expect(document.title).toMatch(
|
||||
`${sequence.title} | ${section.title} | ${course.title} | edX`,
|
||||
);
|
||||
const { models } = store.getState();
|
||||
const sequence = models.sequences[mockData.sequenceId];
|
||||
const section = models.sections[sequence.sectionId];
|
||||
const course = models.coursewareMeta[mockData.courseId];
|
||||
expect(document.title).toMatch(
|
||||
`${sequence.title} | ${section.title} | ${course.title} | edX`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('removes breadcrumbs when navigation is disabled', async () => {
|
||||
@@ -114,9 +109,11 @@ describe('Course', () => {
|
||||
handleNextSectionCelebration(sequenceId, sequenceId, testData.unitId);
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
|
||||
const firstSectionCelebrationModal = screen.getByRole('dialog');
|
||||
expect(firstSectionCelebrationModal).toBeInTheDocument();
|
||||
expect(getByRole(firstSectionCelebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
const firstSectionCelebrationModal = screen.getByRole('dialog');
|
||||
expect(firstSectionCelebrationModal).toBeInTheDocument();
|
||||
expect(getByRole(firstSectionCelebrationModal, 'heading', { name: 'Congratulations!' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays weekly goal celebration modal', async () => {
|
||||
@@ -132,40 +129,40 @@ describe('Course', () => {
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
|
||||
const weeklyGoalCelebrationModal = screen.getByRole('dialog');
|
||||
expect(weeklyGoalCelebrationModal).toBeInTheDocument();
|
||||
expect(getByRole(weeklyGoalCelebrationModal, 'heading', { name: 'You met your goal!' })).toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
const weeklyGoalCelebrationModal = screen.getByRole('dialog');
|
||||
expect(weeklyGoalCelebrationModal).toBeInTheDocument();
|
||||
expect(getByRole(weeklyGoalCelebrationModal, 'heading', { name: 'You met your goal!' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays notification trigger and toggles active class on click', async () => {
|
||||
render(<Course {...mockData} />, { wrapWithRouter: true });
|
||||
|
||||
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
|
||||
expect(notificationTrigger).toBeInTheDocument();
|
||||
expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true });
|
||||
fireEvent.click(notificationTrigger);
|
||||
expect(notificationTrigger.parentNode).toHaveClass('sidebar-active');
|
||||
waitFor(() => {
|
||||
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
|
||||
expect(notificationTrigger).toBeInTheDocument();
|
||||
expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true });
|
||||
fireEvent.click(notificationTrigger);
|
||||
expect(notificationTrigger.parentNode).toHaveClass('sidebar-active');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles click to open/close discussions sidebar', async () => {
|
||||
await setupDiscussionSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
|
||||
});
|
||||
|
||||
const discussionsTrigger = await screen.getByRole('button', { name: /Show discussions tray/i });
|
||||
expect(discussionsTrigger).toBeInTheDocument();
|
||||
fireEvent.click(discussionsTrigger);
|
||||
const discussionsTrigger = screen.getByRole('button', { name: /Show discussions tray/i });
|
||||
expect(discussionsTrigger).toBeInTheDocument();
|
||||
fireEvent.click(discussionsTrigger);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sidebar-DISCUSSIONS')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(discussionsTrigger);
|
||||
fireEvent.click(discussionsTrigger);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -186,9 +183,9 @@ describe('Course', () => {
|
||||
const { rerender } = render(<Course {...testData} />, { store: testStore });
|
||||
loadUnit();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
|
||||
waitFor(() => {
|
||||
expect(screen.findByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
|
||||
expect(screen.findByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
|
||||
});
|
||||
|
||||
rerender(null);
|
||||
@@ -196,11 +193,13 @@ describe('Course', () => {
|
||||
|
||||
it('handles click to open/close notification tray', async () => {
|
||||
await setupDiscussionSidebar();
|
||||
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
|
||||
fireEvent.click(notificationShowButton);
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
|
||||
waitFor(() => {
|
||||
const notificationShowButton = screen.findByRole('button', { name: /Show notification tray/i });
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
|
||||
fireEvent.click(notificationShowButton);
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
@@ -224,10 +223,14 @@ describe('Course', () => {
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
|
||||
loadUnit();
|
||||
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
|
||||
});
|
||||
// expect the section and sequence "titles" to be loaded in as breadcrumb labels.
|
||||
expect(screen.getByText(Object.values(models.sections)[0].title)).toBeInTheDocument();
|
||||
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
expect(screen.findByText(Object.values(models.sections)[0].title)).toBeInTheDocument();
|
||||
expect(screen.findByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('passes handlers to the sequence', async () => {
|
||||
@@ -256,14 +259,16 @@ describe('Course', () => {
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
|
||||
loadUnit();
|
||||
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
|
||||
screen.getAllByRole('link', { name: /previous/i }).forEach(link => fireEvent.click(link));
|
||||
screen.getAllByRole('link', { name: /next/i }).forEach(link => fireEvent.click(link));
|
||||
waitFor(() => {
|
||||
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
|
||||
screen.getAllByRole('link', { name: /previous/i }).forEach(link => fireEvent.click(link));
|
||||
screen.getAllByRole('link', { name: /next/i }).forEach(link => fireEvent.click(link));
|
||||
|
||||
// We are in the middle of the sequence, so no
|
||||
expect(previousSequenceHandler).not.toHaveBeenCalled();
|
||||
expect(nextSequenceHandler).not.toHaveBeenCalled();
|
||||
expect(unitNavigationHandler).toHaveBeenCalledTimes(4);
|
||||
// We are in the middle of the sequence, so no
|
||||
expect(previousSequenceHandler).not.toHaveBeenCalled();
|
||||
expect(nextSequenceHandler).not.toHaveBeenCalled();
|
||||
expect(unitNavigationHandler).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sequence alerts display', () => {
|
||||
@@ -283,7 +288,7 @@ describe('Course', () => {
|
||||
sequenceId: sequenceBlocks[0].id,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
await waitFor(() => expect(screen.getByText('Some random banner text to display.')).toBeInTheDocument());
|
||||
waitFor(() => expect(screen.findByText('Some random banner text to display.')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders Entrance Exam alert with passing score', async () => {
|
||||
@@ -317,7 +322,7 @@ describe('Course', () => {
|
||||
sequenceId: sequenceBlocks[0].id,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
await waitFor(() => expect(screen.getByText('Your score is 100%. You have passed the entrance exam.')).toBeInTheDocument());
|
||||
waitFor(() => expect(screen.findByText('Your score is 100%. You have passed the entrance exam.')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders Entrance Exam alert with non-passing score', async () => {
|
||||
@@ -351,7 +356,7 @@ describe('Course', () => {
|
||||
sequenceId: sequenceBlocks[0].id,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
await waitFor(() => expect(screen.getByText('To access course materials, you must score 70% or higher on this exam. Your current score is 30%.')).toBeInTheDocument());
|
||||
waitFor(() => expect(screen.findByText('To access course materials, you must score 70% or higher on this exam. Your current score is 30%.')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -370,7 +375,7 @@ describe('Course', () => {
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
const chat = screen.queryByTestId(mockChatTestId);
|
||||
await expect(chat).toBeInTheDocument();
|
||||
waitFor(() => expect(chat).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('does not display chat when screen is too narrow (mobile)', async () => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StatefulButton } from '@openedx/paragon';
|
||||
import { Icon, StatefulButton } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import BookmarkOutlineIcon from './BookmarkOutlineIcon';
|
||||
import BookmarkFilledIcon from './BookmarkFilledIcon';
|
||||
import { Bookmark, BookmarkBorder } from '@openedx/paragon/icons';
|
||||
import { removeBookmark, addBookmark } from './data/thunks';
|
||||
|
||||
const addBookmarkLabel = (
|
||||
@@ -42,10 +41,11 @@ const BookmarkButton = ({
|
||||
return (
|
||||
<StatefulButton
|
||||
variant="link"
|
||||
className="px-1 ml-n1 btn-sm text-primary-500"
|
||||
className={`px-1 ml-n1 btn-sm text-primary-500 ${isProcessing && 'disabled'}`}
|
||||
onClick={toggleBookmark}
|
||||
state={state}
|
||||
disabledStates={['defaultProcessing', 'bookmarkedProcessing']}
|
||||
aria-busy={isProcessing}
|
||||
disabled={isProcessing}
|
||||
labels={{
|
||||
default: addBookmarkLabel,
|
||||
defaultProcessing: addBookmarkLabel,
|
||||
@@ -53,10 +53,10 @@ const BookmarkButton = ({
|
||||
bookmarkedProcessing: hasBookmarkLabel,
|
||||
}}
|
||||
icons={{
|
||||
default: <BookmarkOutlineIcon className="text-primary" />,
|
||||
defaultProcessing: <BookmarkOutlineIcon className="text-primary" />,
|
||||
bookmarked: <BookmarkFilledIcon className="text-primary" />,
|
||||
bookmarkedProcessing: <BookmarkFilledIcon className="text-primary" />,
|
||||
default: <Icon src={BookmarkBorder} className="text-primary" />,
|
||||
defaultProcessing: <Icon src={BookmarkBorder} className="text-primary" />,
|
||||
bookmarked: <Icon src={Bookmark} className="text-primary" />,
|
||||
bookmarkedProcessing: <Icon src={Bookmark} className="text-primary" />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user