Compare commits
215 Commits
abdullahwa
...
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 | ||
|
|
7efe8f5cc3 | ||
|
|
263fe6d1a2 | ||
|
|
76f98d5bb2 | ||
|
|
e0386fe40b | ||
|
|
7d99677acd | ||
|
|
29bc2d9e17 | ||
|
|
27f3e79508 | ||
|
|
ed74bee760 | ||
|
|
c7a81fe07a | ||
|
|
d880aac569 | ||
|
|
072d608c64 | ||
|
|
9437142bc8 | ||
|
|
58c8ec5777 | ||
|
|
07357b9f10 | ||
|
|
1264b4245c | ||
|
|
e3ecee18e3 | ||
|
|
c3d96622e8 | ||
|
|
e577efbd27 | ||
|
|
df361236d0 | ||
|
|
e656f5445c | ||
|
|
f124c0d491 | ||
|
|
cc041ba348 | ||
|
|
257c9dcd7f | ||
|
|
1857b86c7e | ||
|
|
1c3610e9af | ||
|
|
796bbef10b | ||
|
|
799e57f970 | ||
|
|
cf3a91dde0 | ||
|
|
72381a783b | ||
|
|
75f56ea4bd | ||
|
|
a418ba6adb | ||
|
|
2da930f819 | ||
|
|
a2c38112fb | ||
|
|
36535d188d | ||
|
|
79f49032e3 | ||
|
|
9b3b123e45 | ||
|
|
98436b4605 | ||
|
|
7652fa46d1 | ||
|
|
2347ce88cd | ||
|
|
78e5c57bd3 | ||
|
|
108636761c | ||
|
|
5f56828bda | ||
|
|
23e522e893 | ||
|
|
9423a889ba |
4
.env
4
.env
@@ -4,7 +4,7 @@
|
|||||||
NODE_ENV='production'
|
NODE_ENV='production'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME=''
|
ACCESS_TOKEN_COOKIE_NAME=''
|
||||||
AI_TRANSLATIONS_URL=''
|
APP_ID='learning'
|
||||||
BASE_URL=''
|
BASE_URL=''
|
||||||
CONTACT_URL=''
|
CONTACT_URL=''
|
||||||
CREDENTIALS_BASE_URL=''
|
CREDENTIALS_BASE_URL=''
|
||||||
@@ -14,7 +14,6 @@ DISCOVERY_API_BASE_URL=''
|
|||||||
DISCUSSIONS_MFE_BASE_URL=''
|
DISCUSSIONS_MFE_BASE_URL=''
|
||||||
ECOMMERCE_BASE_URL=''
|
ECOMMERCE_BASE_URL=''
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NEW_SIDEBAR=''
|
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||||
EXAMS_BASE_URL=''
|
EXAMS_BASE_URL=''
|
||||||
@@ -49,3 +48,4 @@ TWITTER_HASHTAG=''
|
|||||||
TWITTER_URL=''
|
TWITTER_URL=''
|
||||||
USER_INFO_COOKIE_NAME=''
|
USER_INFO_COOKIE_NAME=''
|
||||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
NODE_ENV='development'
|
NODE_ENV='development'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
AI_TRANSLATIONS_URL='http://localhost:18760'
|
APP_ID='learning'
|
||||||
BASE_URL='http://localhost:2000'
|
BASE_URL='http://localhost:2000'
|
||||||
CONTACT_URL='http://localhost:18000/contact'
|
CONTACT_URL='http://localhost:18000/contact'
|
||||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||||
@@ -14,7 +14,6 @@ DISCOVERY_API_BASE_URL='http://localhost:18381'
|
|||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NEW_SIDEBAR=''
|
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
EXAMS_BASE_URL=''
|
EXAMS_BASE_URL=''
|
||||||
@@ -51,3 +50,4 @@ SESSION_COOKIE_DOMAIN='localhost'
|
|||||||
CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
|
CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
|
||||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
NODE_ENV='test'
|
NODE_ENV='test'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
AI_TRANSLATIONS_URL='http://localhost:18760'
|
APP_ID='learning'
|
||||||
BASE_URL='http://localhost:2000'
|
BASE_URL='http://localhost:2000'
|
||||||
CONTACT_URL='http://localhost:18000/contact'
|
CONTACT_URL='http://localhost:18000/contact'
|
||||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||||
@@ -14,7 +14,6 @@ DISCOVERY_API_BASE_URL='http://localhost:18381'
|
|||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NEW_SIDEBAR=''
|
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
EXAMS_BASE_URL='http://localhost:18740'
|
EXAMS_BASE_URL='http://localhost:18740'
|
||||||
@@ -48,3 +47,4 @@ TWITTER_HASHTAG='myedxjourney'
|
|||||||
TWITTER_URL='https://twitter.com/edXOnline'
|
TWITTER_URL='https://twitter.com/edXOnline'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ dist/
|
|||||||
packages/
|
packages/
|
||||||
node_modules/
|
node_modules/
|
||||||
jest.config.js
|
jest.config.js
|
||||||
|
env.config.jsx
|
||||||
|
example.env.config.jsx
|
||||||
|
|||||||
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:
|
jobs:
|
||||||
version-check:
|
version-check:
|
||||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||||
|
|||||||
25
.github/workflows/validate.yml
vendored
25
.github/workflows/validate.yml
vendored
@@ -10,14 +10,27 @@ jobs:
|
|||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Nodejs Env
|
- uses: actions/setup-node@v4
|
||||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VER }}
|
node-version-file: '.nvmrc'
|
||||||
- run: make validate.ci
|
- 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
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
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
|
make validate-no-uncommitted-package-lock-changes
|
||||||
npm run i18n_extract
|
npm run i18n_extract
|
||||||
npm run lint -- --max-warnings 0
|
npm run lint -- --max-warnings 0
|
||||||
|
npm run types
|
||||||
npm run test
|
npm run test
|
||||||
npm run build
|
npm run build
|
||||||
|
npm run bundlewatch
|
||||||
|
|
||||||
.PHONY: validate.ci
|
.PHONY: validate.ci
|
||||||
validate.ci:
|
validate.ci:
|
||||||
|
|||||||
120
README.rst
120
README.rst
@@ -1,77 +1,110 @@
|
|||||||
#####################
|
|
||||||
frontend-app-learning
|
frontend-app-learning
|
||||||
#####################
|
#####################
|
||||||
|
|
||||||
|codecov| |license|
|
|codecov| |license|
|
||||||
|
|
||||||
********
|
|
||||||
Purpose
|
Purpose
|
||||||
********
|
*******
|
||||||
|
|
||||||
This is the Learning MFE (micro-frontend application), which renders all
|
This is the Learning MFE (micro-frontend application), which renders all
|
||||||
learner-facing course pages (like the course outline, the progress page,
|
learner-facing course pages (like the course outline, the progress page,
|
||||||
actual course content, etc).
|
actual course content, etc).
|
||||||
|
|
||||||
Please tag **@edx/engage-squad** on any PRs or issues. Thanks.
|
|
||||||
|
|
||||||
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
||||||
:target: https://codecov.io/gh/edx/frontend-app-learning
|
:target: https://codecov.io/gh/edx/frontend-app-learning
|
||||||
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
||||||
:target: https://github.com/openedx/frontend-app-account/blob/master/LICENSE
|
:target: https://github.com/openedx/frontend-app-account/blob/master/LICENSE
|
||||||
|
|
||||||
***************
|
|
||||||
Getting Started
|
Getting Started
|
||||||
***************
|
***************
|
||||||
|
|
||||||
Prerequisites
|
Prerequisites
|
||||||
=============
|
=============
|
||||||
|
|
||||||
The `devstack`_ is currently recommended as a development environment for your
|
`Tutor`_ is currently recommended as a development environment for the Learning
|
||||||
new MFE. If you start it with ``make dev.up.lms`` that should give you
|
MFE. Most likely, it already has this MFE configured; however, you'll need to
|
||||||
everything you need as a companion to this frontend.
|
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
|
||||||
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
|
guide below.
|
||||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
|
||||||
|
|
||||||
.. _Devstack: https://github.com/openedx/devstack
|
|
||||||
|
|
||||||
.. _Tutor: https://github.com/overhangio/tutor
|
.. _Tutor: https://github.com/overhangio/tutor
|
||||||
|
|
||||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
|
.. _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 Setup
|
||||||
|
=================
|
||||||
|
|
||||||
Cloning and Startup
|
1. Clone your new repo:
|
||||||
===================
|
|
||||||
|
|
||||||
.. code-block::
|
.. code-block:: bash
|
||||||
|
|
||||||
1. Clone your new repo:
|
git clone https://github.com/openedx/frontend-app-learning.git
|
||||||
|
|
||||||
``git clone https://github.com/openedx/frontend-app-learning.git``
|
2. Use node v20.x.
|
||||||
|
|
||||||
2. Use node v18.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>`_.
|
||||||
|
|
||||||
The current version of the micro-frontend build scripts support node 18.
|
3. Stop the Tutor devstack, if it's running: ``tutor dev stop``
|
||||||
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:
|
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:
|
||||||
|
|
||||||
``cd frontend-app-learning && npm ci``
|
.. code-block:: bash
|
||||||
|
|
||||||
4. Start the dev server:
|
tutor mounts add /path/to/frontend-app-learning
|
||||||
|
|
||||||
``npm start``
|
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
|
||||||
|
|
||||||
|
2. Start the dev server:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
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
|
Local module development
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
To develop locally on modules that are installed into this app, you'll need to create a ``module.config.js``
|
To develop locally on modules that are installed into this app, you'll need to create a ``module.config.js``
|
||||||
file (which is git-ignored) that defines where to find your local modules, for instance::
|
file (which is git-ignored) that defines where to find your local modules, for instance:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/*
|
/*
|
||||||
@@ -100,8 +133,14 @@ The Learning MFE is similar to all the other Open edX MFEs. Read the Open
|
|||||||
edX Developer Guide's section on
|
edX Developer Guide's section on
|
||||||
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
|
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
=======
|
||||||
|
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||||
|
|
||||||
|
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||||
|
|
||||||
Environment Variables
|
Environment Variables
|
||||||
======================
|
=====================
|
||||||
|
|
||||||
This MFE is configured via environment variables supplied at build time.
|
This MFE is configured via environment variables supplied at build time.
|
||||||
All micro-frontends have a shared set of required environment variables,
|
All micro-frontends have a shared set of required environment variables,
|
||||||
@@ -127,7 +166,7 @@ SOCIAL_UTM_MILESTONE_CAMPAIGN
|
|||||||
|
|
||||||
SUPPORT_URL_CALCULATOR_MATH
|
SUPPORT_URL_CALCULATOR_MATH
|
||||||
A link that explains how to use the in-course calculator. You can use the
|
A link that explains how to use the in-course calculator. You can use the
|
||||||
one in the example below, if you don't want to have your own branded version.
|
one in the example below if you don't want to have your own branded version.
|
||||||
|
|
||||||
Example: https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator
|
Example: https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator
|
||||||
|
|
||||||
@@ -140,7 +179,7 @@ SUPPORT_URL_ID_VERIFICATION
|
|||||||
|
|
||||||
SUPPORT_URL_VERIFIED_CERTIFICATE
|
SUPPORT_URL_VERIFIED_CERTIFICATE
|
||||||
A link that explains what a verified certificate is. You can use the
|
A link that explains what a verified certificate is. You can use the
|
||||||
one in the example below, if you don't want to have your own branded version.
|
one in the example below if you don't want to have your own branded version.
|
||||||
Optional.
|
Optional.
|
||||||
|
|
||||||
Example: https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate
|
Example: https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate
|
||||||
@@ -156,13 +195,13 @@ TWITTER_URL
|
|||||||
A link to your Twitter account. The Twitter social-share link won't appear
|
A link to your Twitter account. The Twitter social-share link won't appear
|
||||||
unless this is set. Optional.
|
unless this is set. Optional.
|
||||||
|
|
||||||
Example: https://twitter.com/edXOnline
|
Example: https://twitter.com/openedx
|
||||||
|
|
||||||
Getting Help
|
Getting Help
|
||||||
===========
|
============
|
||||||
|
|
||||||
If you're having trouble, we have discussion forums at
|
If you're having trouble, we have `discussion forums`_
|
||||||
https://discuss.openedx.org where you can connect with others in the community.
|
where you can connect with others in the community.
|
||||||
|
|
||||||
Our real-time conversations are on Slack. You can request a `Slack
|
Our real-time conversations are on Slack. You can request a `Slack
|
||||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||||
@@ -180,17 +219,18 @@ For more information about these options, see the `Getting Help`_ page.
|
|||||||
.. _community Slack workspace: https://openedx.slack.com/
|
.. _community Slack workspace: https://openedx.slack.com/
|
||||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||||
.. _Getting Help: https://openedx.org/community/connect
|
.. _Getting Help: https://openedx.org/community/connect
|
||||||
|
.. _discussion forums: https://discuss.openedx.org
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
============
|
============
|
||||||
|
|
||||||
Contributions are very welcome. Please read `How To Contribute`_ for details.
|
Contributions are very welcome. Please read `How To Contribute`_ for details.
|
||||||
|
|
||||||
.. _How To Contribute: https://openedx.org/r/how-to-contribute
|
.. _How To Contribute: https://openedx.org/r/how-to-contribute
|
||||||
|
|
||||||
This project is currently accepting all types of contributions, bug fixes,
|
This project is currently accepting all types of contributions, bug fixes,
|
||||||
security fixes, maintenance work, or new features. However, please make sure
|
security fixes, maintenance work, or new features. However, please make sure
|
||||||
to have a discussion about your new feature idea with the maintainers prior to
|
to discuss your new feature idea with the maintainers before
|
||||||
beginning development to maximize the chances of your change being accepted.
|
beginning development to maximize the chances of your change being accepted.
|
||||||
You can start a conversation by creating a new issue on this repo summarizing
|
You can start a conversation by creating a new issue on this repo summarizing
|
||||||
your idea.
|
your idea.
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ metadata:
|
|||||||
icon: "Web"
|
icon: "Web"
|
||||||
annotations:
|
annotations:
|
||||||
openedx.org/arch-interest-groups: ""
|
openedx.org/arch-interest-groups: ""
|
||||||
|
openedx.org/release: "master"
|
||||||
spec:
|
spec:
|
||||||
owner: group:2u-aurora
|
owner: group:committers-frontend-app-learning
|
||||||
type: 'website'
|
type: 'website'
|
||||||
lifecycle: 'production'
|
lifecycle: 'production'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import UnitTranslationPlugin from '@plugins/UnitTranslationPlugin';
|
import UnitTranslationPlugin from '@edx/unit-translation-selector-plugin';
|
||||||
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
|
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ const config = createConfig('jest', {
|
|||||||
'src/i18n',
|
'src/i18n',
|
||||||
'src/.*\\.exp\\..*',
|
'src/.*\\.exp\\..*',
|
||||||
],
|
],
|
||||||
// see https://github.com/axios/axios/issues/5026
|
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"^axios$": "axios/dist/axios.js",
|
|
||||||
// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
|
// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
|
||||||
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
|
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
|
||||||
'@src/(.*)': '<rootDir>/src/$1',
|
'@src/(.*)': '<rootDir>/src/$1',
|
||||||
'@plugins/(.*)': '<rootDir>/plugins/$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,
|
testTimeout: 30000,
|
||||||
globalSetup: "./global-setup.js",
|
globalSetup: "./global-setup.js",
|
||||||
@@ -27,7 +26,7 @@ const config = createConfig('jest', {
|
|||||||
|
|
||||||
config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
|
config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
|
||||||
// change this setting if need to see less details for each test
|
// change this setting if need to see less details for each test
|
||||||
// reportType: "summary" | "details",
|
// reportType: "summary" | "details",
|
||||||
// enable: true | false,
|
// enable: true | false,
|
||||||
afterEachTest: {
|
afterEachTest: {
|
||||||
enable: true,
|
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
|
|
||||||
14111
package-lock.json
generated
14111
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
89
package.json
89
package.json
@@ -11,13 +11,17 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "fedx-scripts webpack",
|
||||||
|
"bundlewatch": "bundlewatch",
|
||||||
"i18n_extract": "fedx-scripts formatjs extract",
|
"i18n_extract": "fedx-scripts formatjs extract",
|
||||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||||
"prepare": "husky install",
|
"postinstall": "patch-package",
|
||||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"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",
|
"author": "edX",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
@@ -29,31 +33,33 @@
|
|||||||
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@datadog/browser-logs": "^5.14.0",
|
|
||||||
"@datadog/browser-rum": "^5.14.0",
|
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/frontend-component-footer": "^13.0.4",
|
"@edx/browserslist-config": "1.5.0",
|
||||||
"@edx/frontend-component-header": "^5.0.2",
|
"@edx/frontend-component-header": "^5.8.0",
|
||||||
"@edx/frontend-lib-learning-assistant": "^2.0.0",
|
"@edx/frontend-lib-learning-assistant": "^2.19.2",
|
||||||
"@edx/frontend-lib-special-exams": "^3.0.0",
|
"@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/openedx-atlas": "^0.6.0",
|
||||||
"@edx/react-unit-test-utils": "^2.0.0",
|
"@edx/react-unit-test-utils": "3.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||||
"@openedx/frontend-plugin-framework": "^1.0.2",
|
"@openedx/frontend-build": "^14.2.0",
|
||||||
"@openedx/paragon": "^22.1.1",
|
"@openedx/frontend-plugin-framework": "^1.2.1",
|
||||||
|
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||||
|
"@openedx/paragon": "^22.3.0",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@reduxjs/toolkit": "1.8.1",
|
"@reduxjs/toolkit": "1.9.7",
|
||||||
"classnames": "2.3.2",
|
"buffer": "^6.0.3",
|
||||||
"core-js": "3.22.2",
|
"classnames": "2.5.1",
|
||||||
"history": "5.3.0",
|
"copy-webpack-plugin": "^12.0.0",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lodash.camelcase": "4.3.0",
|
"lodash.camelcase": "4.3.0",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
|
"postcss-loader": "^8.1.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
@@ -63,34 +69,35 @@
|
|||||||
"react-router": "6.15.0",
|
"react-router": "6.15.0",
|
||||||
"react-router-dom": "6.15.0",
|
"react-router-dom": "6.15.0",
|
||||||
"react-share": "4.4.1",
|
"react-share": "4.4.1",
|
||||||
"redux": "4.1.2",
|
"redux": "4.2.1",
|
||||||
"regenerator-runtime": "0.13.11",
|
|
||||||
"reselect": "4.1.8",
|
"reselect": "4.1.8",
|
||||||
"truncate-html": "1.0.4",
|
"sass": "^1.79.3",
|
||||||
"util": "0.12.5"
|
"sass-loader": "^16.0.2",
|
||||||
|
"source-map-loader": "^5.0.0",
|
||||||
|
"truncate-html": "1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/browserslist-config": "1.2.0",
|
|
||||||
"@edx/reactifex": "2.2.0",
|
"@edx/reactifex": "2.2.0",
|
||||||
"@openedx/frontend-build": "13.0.30",
|
"@pact-foundation/pact": "^13.0.0",
|
||||||
"@pact-foundation/pact": "^11.0.2",
|
"@testing-library/jest-dom": "5.17.0",
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
"@testing-library/user-event": "13.5.0",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"axios-mock-adapter": "1.20.0",
|
"axios-mock-adapter": "2.1.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"bundlewatch": "^0.4.0",
|
||||||
"es-check": "6.2.1",
|
"eslint-import-resolver-webpack": "^0.13.9",
|
||||||
"eslint-import-resolver-webpack": "^0.13.8",
|
"jest": "^29.7.0",
|
||||||
"husky": "7.0.4",
|
"jest-console-group-reporter": "^1.1.1",
|
||||||
"jest": "^26.6.3",
|
|
||||||
"jest-console-group-reporter": "^1.0.1",
|
|
||||||
"jest-when": "^3.6.0",
|
"jest-when": "^3.6.0",
|
||||||
"postcss-loader": "^8.1.1",
|
"rosie": "2.1.1"
|
||||||
"rosie": "2.1.1",
|
},
|
||||||
"sass": "^1.72.0",
|
"bundlewatch": {
|
||||||
"sass-loader": "^14.1.1",
|
"files": [
|
||||||
"source-map-loader": "^5.0.0",
|
{
|
||||||
"style-loader": "^3.3.4"
|
"path": "dist/*.js",
|
||||||
|
"maxSize": "1400kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
patches/@openedx+frontend-build+13.0.30.patch
Normal file
36
patches/@openedx+frontend-build+13.0.30.patch
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||||
|
index 2879dd9..9efd0fc 100644
|
||||||
|
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||||
|
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||||
|
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
+const fs = require('fs');
|
||||||
|
const PostCssAutoprefixerPlugin = require('autoprefixer');
|
||||||
|
const PostCssRTLCSS = require('postcss-rtlcss');
|
||||||
|
const PostCssCustomMediaCSS = require('postcss-custom-media');
|
||||||
|
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
|
||||||
|
const commonConfig = require('./webpack.common.config');
|
||||||
|
const presets = require('../lib/presets');
|
||||||
|
|
||||||
|
+/**
|
||||||
|
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
|
||||||
|
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
|
||||||
|
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
|
||||||
|
+ * root directory. Its env variables can be accessed with getConfig().
|
||||||
|
+ *
|
||||||
|
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
|
||||||
|
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
|
||||||
|
+ */
|
||||||
|
+
|
||||||
|
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
|
||||||
|
+
|
||||||
|
+if (envConfigPath) {
|
||||||
|
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
|
||||||
|
+ fs.copyFileSync(envConfigPath, envConfigFilename);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(process.cwd(), '.env'),
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
## How to develop plugin
|
|
||||||
|
|
||||||
You can define plugin in `env.config.jsx` see `example.env.config.jsx` as example.
|
|
||||||
|
|
||||||
## Current caveat
|
|
||||||
|
|
||||||
- The way for how I deal with override method is still wonky
|
|
||||||
- The redux still require middleware to ignore the plugin's action from serializing
|
|
||||||
- I am not sure how it behave with useCallback, useMemo, ...etc
|
|
||||||
- There are still open question on how to write it properly
|
|
||||||
|
|
||||||
## Current work that should consider core part and extendable for the future plugin framework
|
|
||||||
|
|
||||||
- `usePluingsCallback` is the callback supose to be some level of equality to be using `React.useCallback`. It would try to execute the function, then any plugin that try `registerOverrideMethod`. The order of the it being run isn't the determined. There are a couple things I want to add:
|
|
||||||
- I might consider testing it with `zustand` library to make sure it is portable and not rely on `redux`. I tried to do this with provider, but it seems to run into infinite loop of trigger changed.
|
|
||||||
|
|
||||||
- `registerOverrideMethod` is working like a way to register callback that behave like a middleware. It ran the default one, then pass the result of the default one to the plugin. Any plugin that register the override can update the value. Alternatively, we can override the function completely instead applying each affect. Or we can support both. But it requires a bit more thought out architecture.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<UnitTranslationPlugin /> render TranslationSelection when translation is enabled and language is available 1`] = `
|
|
||||||
<TranslationSelection
|
|
||||||
availableLanguages={
|
|
||||||
Array [
|
|
||||||
"en",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
courseId="courseId"
|
|
||||||
id="id"
|
|
||||||
language="en"
|
|
||||||
unitId="unitId"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { getConfig, camelCaseObject } from '@edx/frontend-platform';
|
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|
||||||
import { logError } from '@edx/frontend-platform/logging';
|
|
||||||
import { stringify } from 'query-string';
|
|
||||||
|
|
||||||
export const fetchTranslationConfig = async (courseId) => {
|
|
||||||
const url = `${
|
|
||||||
getConfig().LMS_BASE_URL
|
|
||||||
}/api/translatable_xblocks/config/?course_id=${encodeURIComponent(courseId)}`;
|
|
||||||
try {
|
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
|
||||||
return {
|
|
||||||
enabled: data.feature_enabled,
|
|
||||||
availableLanguages: data.available_translation_languages || [
|
|
||||||
{
|
|
||||||
code: 'en',
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'es',
|
|
||||||
label: 'Spanish',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logError(`Translation plugin fail to fetch from ${url}`, error);
|
|
||||||
return {
|
|
||||||
enabled: false,
|
|
||||||
availableLanguages: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getTranslationFeedback({
|
|
||||||
courseId,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
}) {
|
|
||||||
const params = stringify({
|
|
||||||
translation_language: translationLanguage,
|
|
||||||
course_id: encodeURIComponent(courseId),
|
|
||||||
unit_id: encodeURIComponent(unitId),
|
|
||||||
user_id: userId,
|
|
||||||
});
|
|
||||||
const fetchFeedbackUrl = `${
|
|
||||||
getConfig().AI_TRANSLATIONS_URL
|
|
||||||
}/api/v1/whole-course-translation-feedback?${params}`;
|
|
||||||
try {
|
|
||||||
const { data } = await getAuthenticatedHttpClient().get(fetchFeedbackUrl);
|
|
||||||
return camelCaseObject(data);
|
|
||||||
} catch (error) {
|
|
||||||
logError(
|
|
||||||
`Translation plugin fail to fetch from ${fetchFeedbackUrl}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTranslationFeedback({
|
|
||||||
courseId,
|
|
||||||
feedbackValue,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
}) {
|
|
||||||
const createFeedbackUrl = `${
|
|
||||||
getConfig().AI_TRANSLATIONS_URL
|
|
||||||
}/api/v1/whole-course-translation-feedback/`;
|
|
||||||
try {
|
|
||||||
const { data } = await getAuthenticatedHttpClient().post(
|
|
||||||
createFeedbackUrl,
|
|
||||||
{
|
|
||||||
course_id: courseId,
|
|
||||||
feedback_value: feedbackValue,
|
|
||||||
translation_language: translationLanguage,
|
|
||||||
unit_id: unitId,
|
|
||||||
user_id: userId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return camelCaseObject(data);
|
|
||||||
} catch (error) {
|
|
||||||
logError(
|
|
||||||
`Translation plugin fail to create feedback from ${createFeedbackUrl}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { camelCaseObject } from '@edx/frontend-platform';
|
|
||||||
import { logError } from '@edx/frontend-platform/logging';
|
|
||||||
import { stringify } from 'query-string';
|
|
||||||
|
|
||||||
import {
|
|
||||||
fetchTranslationConfig,
|
|
||||||
getTranslationFeedback,
|
|
||||||
createTranslationFeedback,
|
|
||||||
} from './api';
|
|
||||||
|
|
||||||
const mockGetMethod = jest.fn();
|
|
||||||
const mockPostMethod = jest.fn();
|
|
||||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
|
||||||
getAuthenticatedHttpClient: () => ({
|
|
||||||
get: mockGetMethod,
|
|
||||||
post: mockPostMethod,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
|
||||||
logError: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('UnitTranslation api', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
describe('fetchTranslationConfig', () => {
|
|
||||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
|
||||||
const expectedResponse = {
|
|
||||||
feature_enabled: true,
|
|
||||||
available_translation_languages: [
|
|
||||||
{
|
|
||||||
code: 'en',
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'es',
|
|
||||||
label: 'Spanish',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
it('should fetch translation config', async () => {
|
|
||||||
const expectedUrl = `http://localhost:18000/api/translatable_xblocks/config/?course_id=${encodeURIComponent(
|
|
||||||
courseId,
|
|
||||||
)}`;
|
|
||||||
mockGetMethod.mockResolvedValueOnce({ data: expectedResponse });
|
|
||||||
const result = await fetchTranslationConfig(courseId);
|
|
||||||
expect(result).toEqual({
|
|
||||||
enabled: true,
|
|
||||||
availableLanguages: expectedResponse.available_translation_languages,
|
|
||||||
});
|
|
||||||
expect(mockGetMethod).toHaveBeenCalledWith(expectedUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return disabled and unavailable languages on error', async () => {
|
|
||||||
mockGetMethod.mockRejectedValueOnce(new Error('error'));
|
|
||||||
const result = await fetchTranslationConfig(courseId);
|
|
||||||
expect(result).toEqual({
|
|
||||||
enabled: false,
|
|
||||||
availableLanguages: [],
|
|
||||||
});
|
|
||||||
expect(logError).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTranslationFeedback', () => {
|
|
||||||
const props = {
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
translationLanguage: 'es',
|
|
||||||
unitId: 'unit-v1:edX+DemoX+Demo_Course+type@video+block@video',
|
|
||||||
userId: 'test_user',
|
|
||||||
};
|
|
||||||
const expectedResponse = {
|
|
||||||
feedback: 'good',
|
|
||||||
};
|
|
||||||
it('should fetch translation feedback', async () => {
|
|
||||||
const params = stringify({
|
|
||||||
translation_language: props.translationLanguage,
|
|
||||||
course_id: encodeURIComponent(props.courseId),
|
|
||||||
unit_id: encodeURIComponent(props.unitId),
|
|
||||||
user_id: props.userId,
|
|
||||||
});
|
|
||||||
const expectedUrl = `http://localhost:18760/api/v1/whole-course-translation-feedback?${params}`;
|
|
||||||
mockGetMethod.mockResolvedValueOnce({ data: expectedResponse });
|
|
||||||
const result = await getTranslationFeedback(props);
|
|
||||||
expect(result).toEqual(camelCaseObject(expectedResponse));
|
|
||||||
expect(mockGetMethod).toHaveBeenCalledWith(expectedUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty object on error', async () => {
|
|
||||||
mockGetMethod.mockRejectedValueOnce(new Error('error'));
|
|
||||||
const result = await getTranslationFeedback(props);
|
|
||||||
expect(result).toEqual({});
|
|
||||||
expect(logError).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createTranslationFeedback', () => {
|
|
||||||
const props = {
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
feedbackValue: 'good',
|
|
||||||
translationLanguage: 'es',
|
|
||||||
unitId: 'unit-v1:edX+DemoX+Demo_Course+type@video+block@video',
|
|
||||||
userId: 'test_user',
|
|
||||||
};
|
|
||||||
it('should create translation feedback', async () => {
|
|
||||||
const expectedUrl = 'http://localhost:18760/api/v1/whole-course-translation-feedback/';
|
|
||||||
mockPostMethod.mockResolvedValueOnce({});
|
|
||||||
await createTranslationFeedback(props);
|
|
||||||
expect(mockPostMethod).toHaveBeenCalledWith(expectedUrl, {
|
|
||||||
course_id: props.courseId,
|
|
||||||
feedback_value: props.feedbackValue,
|
|
||||||
translation_language: props.translationLanguage,
|
|
||||||
unit_id: props.unitId,
|
|
||||||
user_id: props.userId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should log error on failure', async () => {
|
|
||||||
mockPostMethod.mockRejectedValueOnce(new Error('error'));
|
|
||||||
await createTranslationFeedback(props);
|
|
||||||
expect(logError).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<FeedbackWidget /> render feedback widget 1`] = `
|
|
||||||
<div
|
|
||||||
className="d-none"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="sequence w-100"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="ml-4 mr-2"
|
|
||||||
>
|
|
||||||
<ActionRow>
|
|
||||||
Rate this page translation
|
|
||||||
<Spacer />
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
alt="positive-feedback"
|
|
||||||
className="m-1"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="positive-feedback-button"
|
|
||||||
onClick={[MockFunction onThumbsUpClick]}
|
|
||||||
src="ThumbUpOutline"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
alt="negative-feedback"
|
|
||||||
className="mr-2"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="negative-feedback-button"
|
|
||||||
onClick={[MockFunction onThumbsDownClick]}
|
|
||||||
src="ThumbDownOffAlt"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mb-1 text-light action-row-divider"
|
|
||||||
>
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
alt="close-feedback"
|
|
||||||
className="ml-1 mr-2 float-right"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="close-feedback-button"
|
|
||||||
onClick={[MockFunction closeFeedbackWidget]}
|
|
||||||
src="Close"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<FeedbackWidget /> render gratitude text 1`] = `
|
|
||||||
<div
|
|
||||||
className="d-none"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="sequence w-100"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="ml-4 mr-4"
|
|
||||||
>
|
|
||||||
<ActionRow
|
|
||||||
className="m-2 justify-content-center"
|
|
||||||
>
|
|
||||||
Thank you! Your feedback matters.
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<FeedbackWidget /> renders hidden by default 1`] = `
|
|
||||||
<div
|
|
||||||
className="d-none"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="sequence w-100"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="ml-4 mr-2"
|
|
||||||
>
|
|
||||||
<ActionRow>
|
|
||||||
Rate this page translation
|
|
||||||
<Spacer />
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
alt="positive-feedback"
|
|
||||||
className="m-1"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="positive-feedback-button"
|
|
||||||
onClick={[MockFunction onThumbsUpClick]}
|
|
||||||
src="ThumbUpOutline"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
alt="negative-feedback"
|
|
||||||
className="mr-2"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="negative-feedback-button"
|
|
||||||
onClick={[MockFunction onThumbsDownClick]}
|
|
||||||
src="ThumbDownOffAlt"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mb-1 text-light action-row-divider"
|
|
||||||
>
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
alt="close-feedback"
|
|
||||||
className="ml-1 mr-2 float-right"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="close-feedback-button"
|
|
||||||
onClick={[MockFunction closeFeedbackWidget]}
|
|
||||||
src="Close"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="ml-4 mr-4"
|
|
||||||
>
|
|
||||||
<ActionRow
|
|
||||||
className="m-2 justify-content-center"
|
|
||||||
>
|
|
||||||
Thank you! Your feedback matters.
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<FeedbackWidget /> renders show when elemReady is true 1`] = `
|
|
||||||
<div
|
|
||||||
className="sequence-container d-inline-flex flex-row w-100"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="sequence w-100"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="ml-4 mr-2"
|
|
||||||
>
|
|
||||||
<ActionRow>
|
|
||||||
Rate this page translation
|
|
||||||
<Spacer />
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
alt="positive-feedback"
|
|
||||||
className="m-1"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="positive-feedback-button"
|
|
||||||
onClick={[MockFunction onThumbsUpClick]}
|
|
||||||
src="ThumbUpOutline"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
alt="negative-feedback"
|
|
||||||
className="mr-2"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="negative-feedback-button"
|
|
||||||
onClick={[MockFunction onThumbsDownClick]}
|
|
||||||
src="ThumbDownOffAlt"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mb-1 text-light action-row-divider"
|
|
||||||
>
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
alt="close-feedback"
|
|
||||||
className="ml-1 mr-2 float-right"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="close-feedback-button"
|
|
||||||
onClick={[MockFunction closeFeedbackWidget]}
|
|
||||||
src="Close"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="ml-4 mr-4"
|
|
||||||
>
|
|
||||||
<ActionRow
|
|
||||||
className="m-2 justify-content-center"
|
|
||||||
>
|
|
||||||
Thank you! Your feedback matters.
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useEffect, useRef, useState,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { ActionRow, IconButton, Icon } from '@openedx/paragon';
|
|
||||||
import { Close, ThumbUpOutline, ThumbDownOffAlt } from '@openedx/paragon/icons';
|
|
||||||
|
|
||||||
import './index.scss';
|
|
||||||
import messages from './messages';
|
|
||||||
import useFeedbackWidget from './useFeedbackWidget';
|
|
||||||
|
|
||||||
const FeedbackWidget = ({
|
|
||||||
courseId,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
}) => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const ref = useRef(null);
|
|
||||||
const [elemReady, setElemReady] = useState(false);
|
|
||||||
const {
|
|
||||||
closeFeedbackWidget,
|
|
||||||
showFeedbackWidget,
|
|
||||||
showGratitudeText,
|
|
||||||
onThumbsUpClick,
|
|
||||||
onThumbsDownClick,
|
|
||||||
} = useFeedbackWidget({
|
|
||||||
courseId,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ref.current) {
|
|
||||||
const domNode = document.getElementById('whole-course-translation-feedback-widget');
|
|
||||||
domNode.appendChild(ref.current);
|
|
||||||
setElemReady(true);
|
|
||||||
}
|
|
||||||
}, [ref.current]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className={(elemReady) ? 'sequence-container d-inline-flex flex-row w-100' : 'd-none'}>
|
|
||||||
{(showFeedbackWidget || showGratitudeText) ? (
|
|
||||||
<div className="sequence w-100">
|
|
||||||
{
|
|
||||||
showFeedbackWidget && (
|
|
||||||
<div className="ml-4 mr-2">
|
|
||||||
<ActionRow>
|
|
||||||
{formatMessage(messages.rateTranslationText)}
|
|
||||||
<ActionRow.Spacer />
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
src={ThumbUpOutline}
|
|
||||||
iconAs={Icon}
|
|
||||||
alt="positive-feedback"
|
|
||||||
onClick={onThumbsUpClick}
|
|
||||||
variant="secondary"
|
|
||||||
className="m-1"
|
|
||||||
id="positive-feedback-button"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
src={ThumbDownOffAlt}
|
|
||||||
iconAs={Icon}
|
|
||||||
alt="negative-feedback"
|
|
||||||
onClick={onThumbsDownClick}
|
|
||||||
variant="secondary"
|
|
||||||
className="mr-2"
|
|
||||||
id="negative-feedback-button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-1 text-light action-row-divider">
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
src={Close}
|
|
||||||
iconAs={Icon}
|
|
||||||
alt="close-feedback"
|
|
||||||
onClick={closeFeedbackWidget}
|
|
||||||
variant="secondary"
|
|
||||||
className="ml-1 mr-2 float-right"
|
|
||||||
id="close-feedback-button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
showGratitudeText && (
|
|
||||||
<div className="ml-4 mr-4">
|
|
||||||
<ActionRow className="m-2 justify-content-center">
|
|
||||||
{formatMessage(messages.gratitudeText)}
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
FeedbackWidget.propTypes = {
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
translationLanguage: PropTypes.string.isRequired,
|
|
||||||
userId: PropTypes.string.isRequired,
|
|
||||||
unitId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
FeedbackWidget.defaultProps = {};
|
|
||||||
|
|
||||||
export default FeedbackWidget;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.action-row-divider {
|
|
||||||
font-size: 31px;
|
|
||||||
font-weight: 100;
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import FeedbackWidget from './index';
|
|
||||||
import useFeedbackWidget from './useFeedbackWidget';
|
|
||||||
|
|
||||||
jest.mock('react', () => ({
|
|
||||||
...jest.requireActual('react'),
|
|
||||||
useState: jest.fn((value) => [value, jest.fn()]),
|
|
||||||
}));
|
|
||||||
jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({
|
|
||||||
ActionRow: {
|
|
||||||
Spacer: 'Spacer',
|
|
||||||
},
|
|
||||||
IconButton: 'IconButton',
|
|
||||||
Icon: 'Icon',
|
|
||||||
}));
|
|
||||||
jest.mock('@openedx/paragon/icons', () => ({
|
|
||||||
Close: 'Close',
|
|
||||||
ThumbUpOutline: 'ThumbUpOutline',
|
|
||||||
ThumbDownOffAlt: 'ThumbDownOffAlt',
|
|
||||||
}));
|
|
||||||
jest.mock('./useFeedbackWidget');
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
|
||||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
|
||||||
const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
|
|
||||||
return {
|
|
||||||
...i18n,
|
|
||||||
useIntl: jest.fn(() => ({
|
|
||||||
formatMessage,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('<FeedbackWidget />', () => {
|
|
||||||
const props = {
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
translationLanguage: 'es',
|
|
||||||
unitId:
|
|
||||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@37b72b3915204b70acb00c55b604b563',
|
|
||||||
userId: '123',
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockUseFeedbackWidget = ({ showFeedbackWidget, showGratitudeText }) => {
|
|
||||||
useFeedbackWidget.mockReturnValueOnce({
|
|
||||||
closeFeedbackWidget: jest.fn().mockName('closeFeedbackWidget'),
|
|
||||||
sendFeedback: jest.fn().mockName('sendFeedback'),
|
|
||||||
onThumbsUpClick: jest.fn().mockName('onThumbsUpClick'),
|
|
||||||
onThumbsDownClick: jest.fn().mockName('onThumbsDownClick'),
|
|
||||||
showFeedbackWidget,
|
|
||||||
showGratitudeText,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
it('renders hidden by default', () => {
|
|
||||||
mockUseFeedbackWidget({
|
|
||||||
showFeedbackWidget: true,
|
|
||||||
showGratitudeText: true,
|
|
||||||
});
|
|
||||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
expect(wrapper.instance.findByType('div')[0].props.className).toContain(
|
|
||||||
'd-none',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders show when elemReady is true', () => {
|
|
||||||
mockUseFeedbackWidget({
|
|
||||||
showFeedbackWidget: true,
|
|
||||||
showGratitudeText: true,
|
|
||||||
});
|
|
||||||
useState.mockReturnValueOnce([true, jest.fn()]);
|
|
||||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
expect(wrapper.instance.findByType('div')[0].props.className).not.toContain(
|
|
||||||
'd-none',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render empty when showFeedbackWidget and showGratitudeText are false', () => {
|
|
||||||
mockUseFeedbackWidget({
|
|
||||||
showFeedbackWidget: false,
|
|
||||||
showGratitudeText: false,
|
|
||||||
});
|
|
||||||
useState.mockReturnValueOnce([true, jest.fn()]);
|
|
||||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
|
||||||
expect(wrapper.instance.findByType('div')[0].children.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render feedback widget', () => {
|
|
||||||
mockUseFeedbackWidget({
|
|
||||||
showFeedbackWidget: true,
|
|
||||||
showGratitudeText: false,
|
|
||||||
});
|
|
||||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render gratitude text', () => {
|
|
||||||
mockUseFeedbackWidget({
|
|
||||||
showFeedbackWidget: false,
|
|
||||||
showGratitudeText: true,
|
|
||||||
});
|
|
||||||
const wrapper = shallow(<FeedbackWidget {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
rateTranslationText: {
|
|
||||||
id: 'feedbackWidget.rateTranslationText',
|
|
||||||
defaultMessage: 'Rate this page translation',
|
|
||||||
description: 'Title for the feedback widget action row.',
|
|
||||||
},
|
|
||||||
gratitudeText: {
|
|
||||||
id: 'feedbackWidget.gratitudeText',
|
|
||||||
defaultMessage: 'Thank you! Your feedback matters.',
|
|
||||||
description: 'Title for secondary action row.',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { createTranslationFeedback, getTranslationFeedback } from '../data/api';
|
|
||||||
|
|
||||||
const useFeedbackWidget = ({
|
|
||||||
courseId,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
}) => {
|
|
||||||
const [showFeedbackWidget, setShowFeedbackWidget] = useState(false);
|
|
||||||
const [showGratitudeText, setShowGratitudeText] = useState(false);
|
|
||||||
|
|
||||||
const closeFeedbackWidget = useCallback(() => {
|
|
||||||
setShowFeedbackWidget(false);
|
|
||||||
}, [setShowFeedbackWidget]);
|
|
||||||
|
|
||||||
const openFeedbackWidget = useCallback(() => {
|
|
||||||
setShowFeedbackWidget(true);
|
|
||||||
}, [setShowFeedbackWidget]);
|
|
||||||
|
|
||||||
useEffect(async () => {
|
|
||||||
const translationFeedback = await getTranslationFeedback({
|
|
||||||
courseId,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
setShowFeedbackWidget(!translationFeedback);
|
|
||||||
}, [
|
|
||||||
courseId,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const openGratitudeText = useCallback(() => {
|
|
||||||
setShowGratitudeText(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowGratitudeText(false);
|
|
||||||
}, 3000);
|
|
||||||
}, [setShowGratitudeText]);
|
|
||||||
|
|
||||||
const sendFeedback = useCallback(async (feedbackValue) => {
|
|
||||||
await createTranslationFeedback({
|
|
||||||
courseId,
|
|
||||||
feedbackValue,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
closeFeedbackWidget();
|
|
||||||
openGratitudeText();
|
|
||||||
}, [
|
|
||||||
courseId,
|
|
||||||
translationLanguage,
|
|
||||||
unitId,
|
|
||||||
userId,
|
|
||||||
closeFeedbackWidget,
|
|
||||||
openGratitudeText,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onThumbsUpClick = useCallback(() => {
|
|
||||||
sendFeedback(true);
|
|
||||||
}, [sendFeedback]);
|
|
||||||
const onThumbsDownClick = useCallback(() => {
|
|
||||||
sendFeedback(false);
|
|
||||||
}, [sendFeedback]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
closeFeedbackWidget,
|
|
||||||
openFeedbackWidget,
|
|
||||||
openGratitudeText,
|
|
||||||
sendFeedback,
|
|
||||||
showFeedbackWidget,
|
|
||||||
showGratitudeText,
|
|
||||||
onThumbsUpClick,
|
|
||||||
onThumbsDownClick,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFeedbackWidget;
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { renderHook, act } from '@testing-library/react-hooks';
|
|
||||||
|
|
||||||
import useFeedbackWidget from './useFeedbackWidget';
|
|
||||||
import { createTranslationFeedback, getTranslationFeedback } from '../data/api';
|
|
||||||
|
|
||||||
jest.mock('../data/api', () => ({
|
|
||||||
createTranslationFeedback: jest.fn(),
|
|
||||||
getTranslationFeedback: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const initialProps = {
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
translationLanguage: 'es',
|
|
||||||
unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
|
|
||||||
userId: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newProps = {
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
translationLanguage: 'fr',
|
|
||||||
unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
|
|
||||||
userId: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('useFeedbackWidget', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
getTranslationFeedback.mockReturnValue('');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('closeFeedbackWidget behavior', () => {
|
|
||||||
const { result, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
waitFor(() => expect(result.current.showFeedbackWidget.toBe(true)));
|
|
||||||
act(() => {
|
|
||||||
result.current.closeFeedbackWidget();
|
|
||||||
});
|
|
||||||
expect(result.current.showFeedbackWidget).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('openFeedbackWidget behavior', () => {
|
|
||||||
const { result } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
act(() => {
|
|
||||||
result.current.closeFeedbackWidget();
|
|
||||||
});
|
|
||||||
expect(result.current.showFeedbackWidget).toBe(false);
|
|
||||||
act(() => {
|
|
||||||
result.current.openFeedbackWidget();
|
|
||||||
});
|
|
||||||
expect(result.current.showFeedbackWidget).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('openGratitudeText behavior', async () => {
|
|
||||||
const { result, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
|
|
||||||
expect(result.current.showGratitudeText).toBe(false);
|
|
||||||
act(() => {
|
|
||||||
result.current.openGratitudeText();
|
|
||||||
});
|
|
||||||
expect(result.current.showGratitudeText).toBe(true);
|
|
||||||
// Wait for 3 seconds to hide the gratitude text
|
|
||||||
waitFor(() => {
|
|
||||||
expect(result.current.showGratitudeText).toBe(false);
|
|
||||||
}, { timeout: 3000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sendFeedback behavior', () => {
|
|
||||||
const { result, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
const feedbackValue = true;
|
|
||||||
|
|
||||||
waitFor(() => expect(result.current.showFeedbackWidget.toBe(true)));
|
|
||||||
|
|
||||||
expect(result.current.showGratitudeText).toBe(false);
|
|
||||||
act(() => {
|
|
||||||
result.current.sendFeedback(feedbackValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
waitFor(() => {
|
|
||||||
expect(result.current.showFeedbackWidget).toBe(false);
|
|
||||||
expect(result.current.showGratitudeText).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(createTranslationFeedback).toHaveBeenCalledWith({
|
|
||||||
courseId: initialProps.courseId,
|
|
||||||
feedbackValue,
|
|
||||||
translationLanguage: initialProps.translationLanguage,
|
|
||||||
unitId: initialProps.unitId,
|
|
||||||
userId: initialProps.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for 3 seconds to hide the gratitude text
|
|
||||||
waitFor(() => {
|
|
||||||
expect(result.current.showGratitudeText).toBe(false);
|
|
||||||
}, { timeout: 3000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onThumbsUpClick behavior', () => {
|
|
||||||
const { result } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.onThumbsUpClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(createTranslationFeedback).toHaveBeenCalledWith({
|
|
||||||
courseId: initialProps.courseId,
|
|
||||||
feedbackValue: true,
|
|
||||||
translationLanguage: initialProps.translationLanguage,
|
|
||||||
unitId: initialProps.unitId,
|
|
||||||
userId: initialProps.userId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onThumbsDownClick behavior', () => {
|
|
||||||
const { result } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.onThumbsDownClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(createTranslationFeedback).toHaveBeenCalledWith({
|
|
||||||
courseId: initialProps.courseId,
|
|
||||||
feedbackValue: false,
|
|
||||||
translationLanguage: initialProps.translationLanguage,
|
|
||||||
unitId: initialProps.unitId,
|
|
||||||
userId: initialProps.userId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fetch feedback on initialization', () => {
|
|
||||||
const { waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
waitFor(() => {
|
|
||||||
expect(getTranslationFeedback).toHaveBeenCalledWith({
|
|
||||||
courseId: initialProps.courseId,
|
|
||||||
translationLanguage: initialProps.translationLanguage,
|
|
||||||
unitId: initialProps.unitId,
|
|
||||||
userId: initialProps.userId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fetch feedback on props update', () => {
|
|
||||||
const { rerender, waitFor } = renderHook(() => useFeedbackWidget(initialProps));
|
|
||||||
waitFor(() => {
|
|
||||||
expect(getTranslationFeedback).toHaveBeenCalledWith({
|
|
||||||
courseId: initialProps.courseId,
|
|
||||||
translationLanguage: initialProps.translationLanguage,
|
|
||||||
unitId: initialProps.unitId,
|
|
||||||
userId: initialProps.userId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
rerender(newProps);
|
|
||||||
waitFor(() => {
|
|
||||||
expect(getTranslationFeedback).toHaveBeenCalledWith({
|
|
||||||
courseId: newProps.courseId,
|
|
||||||
translationLanguage: newProps.translationLanguage,
|
|
||||||
unitId: newProps.unitId,
|
|
||||||
userId: newProps.userId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { useModel } from '@src/generic/model-store';
|
|
||||||
|
|
||||||
import TranslationSelection from './translation-selection';
|
|
||||||
import { fetchTranslationConfig } from './data/api';
|
|
||||||
|
|
||||||
const UnitTranslationPlugin = ({ id, courseId, unitId }) => {
|
|
||||||
const { language } = useModel('coursewareMeta', courseId);
|
|
||||||
const [translationConfig, setTranslationConfig] = useState({
|
|
||||||
enabled: false,
|
|
||||||
availableLanguages: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTranslationConfig(courseId).then(setTranslationConfig);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { enabled, availableLanguages } = translationConfig;
|
|
||||||
|
|
||||||
if (!enabled || !language || !availableLanguages.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TranslationSelection
|
|
||||||
id={id}
|
|
||||||
courseId={courseId}
|
|
||||||
language={language}
|
|
||||||
availableLanguages={availableLanguages}
|
|
||||||
unitId={unitId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
UnitTranslationPlugin.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
unitId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UnitTranslationPlugin;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useModel } from '@src/generic/model-store';
|
|
||||||
|
|
||||||
import UnitTranslationPlugin from './index';
|
|
||||||
|
|
||||||
jest.mock('@src/generic/model-store');
|
|
||||||
jest.mock('./data/api', () => ({
|
|
||||||
fetchTranslationConfig: jest.fn(),
|
|
||||||
}));
|
|
||||||
jest.mock('./translation-selection', () => 'TranslationSelection');
|
|
||||||
jest.mock('react', () => ({
|
|
||||||
...jest.requireActual('react'),
|
|
||||||
useState: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('<UnitTranslationPlugin />', () => {
|
|
||||||
const props = {
|
|
||||||
id: 'id',
|
|
||||||
courseId: 'courseId',
|
|
||||||
unitId: 'unitId',
|
|
||||||
};
|
|
||||||
const mockInitialState = ({ enabled = true, availableLanguages = ['en'] }) => {
|
|
||||||
useState.mockReturnValue([{ enabled, availableLanguages }, jest.fn()]);
|
|
||||||
};
|
|
||||||
it('render empty when translation is not enabled', () => {
|
|
||||||
useModel.mockReturnValue({ language: 'en' });
|
|
||||||
mockInitialState({ enabled: false });
|
|
||||||
|
|
||||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
|
||||||
|
|
||||||
expect(wrapper.isEmptyRender()).toBe(true);
|
|
||||||
});
|
|
||||||
it('render empty when available languages is empty', () => {
|
|
||||||
useModel.mockReturnValue({ language: 'fr' });
|
|
||||||
mockInitialState({
|
|
||||||
availableLanguages: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
|
||||||
|
|
||||||
expect(wrapper.isEmptyRender()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render empty when course language has not been set', () => {
|
|
||||||
useModel.mockReturnValue({ language: undefined });
|
|
||||||
mockInitialState({});
|
|
||||||
|
|
||||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
|
||||||
|
|
||||||
expect(wrapper.isEmptyRender()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render TranslationSelection when translation is enabled and language is available', () => {
|
|
||||||
useModel.mockReturnValue({ language: 'en' });
|
|
||||||
mockInitialState({});
|
|
||||||
|
|
||||||
const wrapper = shallow(<UnitTranslationPlugin {...props} />);
|
|
||||||
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import {
|
|
||||||
StandardModal,
|
|
||||||
ActionRow,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
ListBox,
|
|
||||||
ListBoxOption,
|
|
||||||
} from '@openedx/paragon';
|
|
||||||
import { Check } from '@openedx/paragon/icons';
|
|
||||||
|
|
||||||
import useTranslationModal from './useTranslationModal';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
import './TranslationModal.scss';
|
|
||||||
|
|
||||||
const TranslationModal = ({
|
|
||||||
isOpen,
|
|
||||||
close,
|
|
||||||
selectedLanguage,
|
|
||||||
setSelectedLanguage,
|
|
||||||
availableLanguages,
|
|
||||||
}) => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const { selectedIndex, setSelectedIndex, onSubmit } = useTranslationModal({
|
|
||||||
selectedLanguage,
|
|
||||||
setSelectedLanguage,
|
|
||||||
close,
|
|
||||||
availableLanguages,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StandardModal
|
|
||||||
title={formatMessage(messages.languageSelectionModalTitle)}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={close}
|
|
||||||
footerNode={(
|
|
||||||
<ActionRow>
|
|
||||||
<ActionRow.Spacer />
|
|
||||||
<Button variant="tertiary" onClick={close}>
|
|
||||||
{formatMessage(messages.cancelButtonText)}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onSubmit}>
|
|
||||||
{formatMessage(messages.submitButtonText)}
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ListBox className="listbox-container">
|
|
||||||
{availableLanguages.map(({ code, label }, index) => (
|
|
||||||
<ListBoxOption
|
|
||||||
className="d-flex justify-content-between"
|
|
||||||
key={code}
|
|
||||||
selectedOptionIndex={selectedIndex}
|
|
||||||
onSelect={() => setSelectedIndex(index)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{selectedIndex === index && <Icon src={Check} />}
|
|
||||||
</ListBoxOption>
|
|
||||||
))}
|
|
||||||
</ListBox>
|
|
||||||
</StandardModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TranslationModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
close: PropTypes.func.isRequired,
|
|
||||||
selectedLanguage: PropTypes.string.isRequired,
|
|
||||||
setSelectedLanguage: PropTypes.func.isRequired,
|
|
||||||
availableLanguages: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
code: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
}),
|
|
||||||
).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TranslationModal;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.listbox-container {
|
|
||||||
max-height: 400px;
|
|
||||||
|
|
||||||
:last-child {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import TranslationModal from './TranslationModal';
|
|
||||||
|
|
||||||
jest.mock('./useTranslationModal', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: () => ({
|
|
||||||
selectedIndex: 0,
|
|
||||||
setSelectedIndex: jest.fn(),
|
|
||||||
onSubmit: jest.fn().mockName('onSubmit'),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({
|
|
||||||
StandardModal: 'StandardModal',
|
|
||||||
ActionRow: {
|
|
||||||
Spacer: 'Spacer',
|
|
||||||
},
|
|
||||||
Button: 'Button',
|
|
||||||
Icon: 'Icon',
|
|
||||||
ListBox: 'ListBox',
|
|
||||||
ListBoxOption: 'ListBoxOption',
|
|
||||||
}));
|
|
||||||
jest.mock('@openedx/paragon/icons', () => ({
|
|
||||||
Check: jest.fn().mockName('icons.Check'),
|
|
||||||
}));
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
|
||||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
|
||||||
const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
|
|
||||||
return {
|
|
||||||
...i18n,
|
|
||||||
useIntl: jest.fn(() => ({
|
|
||||||
formatMessage,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('TranslationModal', () => {
|
|
||||||
const props = {
|
|
||||||
isOpen: true,
|
|
||||||
close: jest.fn().mockName('close'),
|
|
||||||
selectedLanguage: 'en',
|
|
||||||
setSelectedLanguage: jest.fn().mockName('setSelectedLanguage'),
|
|
||||||
availableLanguages: [
|
|
||||||
{
|
|
||||||
code: 'en',
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'es',
|
|
||||||
label: 'Spanish',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
it('renders correctly', () => {
|
|
||||||
const wrapper = shallow(<TranslationModal {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
expect(wrapper.instance.findByType('ListBoxOption')).toHaveLength(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`TranslationModal renders correctly 1`] = `
|
|
||||||
<StandardModal
|
|
||||||
footerNode={
|
|
||||||
<ActionRow>
|
|
||||||
<Spacer />
|
|
||||||
<Button
|
|
||||||
onClick={[MockFunction close]}
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={[MockFunction onSubmit]}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
}
|
|
||||||
isOpen={true}
|
|
||||||
onClose={[MockFunction close]}
|
|
||||||
title="Translate this course"
|
|
||||||
>
|
|
||||||
<ListBox
|
|
||||||
className="listbox-container"
|
|
||||||
>
|
|
||||||
<ListBoxOption
|
|
||||||
className="d-flex justify-content-between"
|
|
||||||
key="en"
|
|
||||||
onSelect={[Function]}
|
|
||||||
selectedOptionIndex={0}
|
|
||||||
>
|
|
||||||
English
|
|
||||||
<Icon
|
|
||||||
src={[MockFunction icons.Check]}
|
|
||||||
/>
|
|
||||||
</ListBoxOption>
|
|
||||||
<ListBoxOption
|
|
||||||
className="d-flex justify-content-between"
|
|
||||||
key="es"
|
|
||||||
onSelect={[Function]}
|
|
||||||
selectedOptionIndex={0}
|
|
||||||
>
|
|
||||||
Spanish
|
|
||||||
</ListBoxOption>
|
|
||||||
</ListBox>
|
|
||||||
</StandardModal>
|
|
||||||
`;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<TranslationSelection /> renders 1`] = `
|
|
||||||
<Fragment>
|
|
||||||
<ProductTour
|
|
||||||
tours={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"abitrarily": "defined",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
alt="change-language"
|
|
||||||
className="mr-2 mb-2 float-right"
|
|
||||||
iconAs="Icon"
|
|
||||||
id="translation-selection-button"
|
|
||||||
onClick={[MockFunction open]}
|
|
||||||
src="Language"
|
|
||||||
variant="primary"
|
|
||||||
/>
|
|
||||||
<TranslationModal
|
|
||||||
availableLanguages={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"code": "en",
|
|
||||||
"label": "English",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"code": "es",
|
|
||||||
"label": "Spanish",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
close={[MockFunction close]}
|
|
||||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
|
||||||
id="plugin-test-id"
|
|
||||||
isOpen={false}
|
|
||||||
selectedLanguage="en"
|
|
||||||
setSelectedLanguage={[MockFunction setSelectedLanguage]}
|
|
||||||
/>
|
|
||||||
<FeedbackWidget
|
|
||||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
|
||||||
translationLanguage="en"
|
|
||||||
unitId="unit-test-id"
|
|
||||||
userId="123"
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import React, { useContext, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
|
||||||
import { IconButton, Icon, ProductTour } from '@openedx/paragon';
|
|
||||||
import { Language } from '@openedx/paragon/icons';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { stringifyUrl } from 'query-string';
|
|
||||||
|
|
||||||
import { registerOverrideMethod } from '@src/generic/plugin-store';
|
|
||||||
|
|
||||||
import TranslationModal from './TranslationModal';
|
|
||||||
import useTranslationTour from './useTranslationTour';
|
|
||||||
import useSelectLanguage from './useSelectLanguage';
|
|
||||||
import FeedbackWidget from '../feedback-widget';
|
|
||||||
|
|
||||||
const TranslationSelection = ({
|
|
||||||
id, courseId, language, availableLanguages, unitId,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
authenticatedUser: { userId },
|
|
||||||
} = useContext(AppContext);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const {
|
|
||||||
translationTour, isOpen, open, close,
|
|
||||||
} = useTranslationTour();
|
|
||||||
|
|
||||||
const { selectedLanguage, setSelectedLanguage } = useSelectLanguage({
|
|
||||||
courseId,
|
|
||||||
language,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(
|
|
||||||
registerOverrideMethod({
|
|
||||||
pluginName: id,
|
|
||||||
methodName: 'getIFrameUrl',
|
|
||||||
method: (iframeUrl) => {
|
|
||||||
const finalUrl = stringifyUrl({
|
|
||||||
url: iframeUrl,
|
|
||||||
query: {
|
|
||||||
...(language
|
|
||||||
&& selectedLanguage
|
|
||||||
&& language !== selectedLanguage && {
|
|
||||||
src_lang: language,
|
|
||||||
dest_lang: selectedLanguage,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return finalUrl;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}, [language, selectedLanguage]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ProductTour tours={[translationTour]} />
|
|
||||||
<IconButton
|
|
||||||
src={Language}
|
|
||||||
iconAs={Icon}
|
|
||||||
alt="change-language"
|
|
||||||
onClick={open}
|
|
||||||
variant="primary"
|
|
||||||
className="mr-2 mb-2 float-right"
|
|
||||||
id="translation-selection-button"
|
|
||||||
/>
|
|
||||||
<TranslationModal
|
|
||||||
isOpen={isOpen}
|
|
||||||
close={close}
|
|
||||||
courseId={courseId}
|
|
||||||
selectedLanguage={selectedLanguage}
|
|
||||||
setSelectedLanguage={setSelectedLanguage}
|
|
||||||
availableLanguages={availableLanguages}
|
|
||||||
id={id}
|
|
||||||
/>
|
|
||||||
<FeedbackWidget
|
|
||||||
courseId={courseId}
|
|
||||||
translationLanguage={selectedLanguage}
|
|
||||||
unitId={unitId}
|
|
||||||
userId={userId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TranslationSelection.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
unitId: PropTypes.string.isRequired,
|
|
||||||
language: PropTypes.string.isRequired,
|
|
||||||
availableLanguages: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
code: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
})).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
TranslationSelection.defaultProps = {};
|
|
||||||
|
|
||||||
export default TranslationSelection;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import TranslationSelection from './index';
|
|
||||||
|
|
||||||
jest.mock('react', () => ({
|
|
||||||
...jest.requireActual('react'),
|
|
||||||
useContext: jest.fn().mockName('useContext').mockReturnValue({
|
|
||||||
authenticatedUser: {
|
|
||||||
userId: '123',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
jest.mock('@openedx/paragon', () => ({
|
|
||||||
IconButton: 'IconButton',
|
|
||||||
Icon: 'Icon',
|
|
||||||
ProductTour: 'ProductTour',
|
|
||||||
}));
|
|
||||||
jest.mock('@openedx/paragon/icons', () => ({
|
|
||||||
Language: 'Language',
|
|
||||||
}));
|
|
||||||
jest.mock('./useTranslationTour', () => () => ({
|
|
||||||
translationTour: {
|
|
||||||
abitrarily: 'defined',
|
|
||||||
},
|
|
||||||
isOpen: false,
|
|
||||||
open: jest.fn().mockName('open'),
|
|
||||||
close: jest.fn().mockName('close'),
|
|
||||||
}));
|
|
||||||
jest.mock('react-redux', () => ({
|
|
||||||
useDispatch: jest.fn().mockName('useDispatch'),
|
|
||||||
}));
|
|
||||||
jest.mock('@src/generic/plugin-store', () => ({
|
|
||||||
registerOverrideMethod: jest.fn().mockName('registerOverrideMethod'),
|
|
||||||
}));
|
|
||||||
jest.mock('./TranslationModal', () => 'TranslationModal');
|
|
||||||
jest.mock('./useSelectLanguage', () => () => ({
|
|
||||||
selectedLanguage: 'en',
|
|
||||||
setSelectedLanguage: jest.fn().mockName('setSelectedLanguage'),
|
|
||||||
}));
|
|
||||||
jest.mock('../feedback-widget', () => 'FeedbackWidget');
|
|
||||||
|
|
||||||
describe('<TranslationSelection />', () => {
|
|
||||||
const props = {
|
|
||||||
id: 'plugin-test-id',
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
language: 'en',
|
|
||||||
availableLanguages: [
|
|
||||||
{
|
|
||||||
code: 'en',
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'es',
|
|
||||||
label: 'Spanish',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
unitId: 'unit-test-id',
|
|
||||||
};
|
|
||||||
it('renders', () => {
|
|
||||||
const wrapper = shallow(<TranslationSelection {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
translationTourModalTitle: {
|
|
||||||
id: 'translationSelection.translationTourModalTitle',
|
|
||||||
defaultMessage: 'This is a standard modal dialog',
|
|
||||||
description: 'Title for the translation modal.',
|
|
||||||
},
|
|
||||||
translationTourModalBody: {
|
|
||||||
id: 'translationSelection.translationTourModalBody',
|
|
||||||
defaultMessage: 'Now you can easily translate course content.',
|
|
||||||
description: 'Body for the translation modal.',
|
|
||||||
},
|
|
||||||
tryItButtonText: {
|
|
||||||
id: 'translationSelection.tryItButtonText',
|
|
||||||
defaultMessage: 'Try it',
|
|
||||||
description: 'Button text for the translation modal.',
|
|
||||||
},
|
|
||||||
dismissButtonText: {
|
|
||||||
id: 'translationSelection.dismissButtonText',
|
|
||||||
defaultMessage: 'Dismiss',
|
|
||||||
description: 'Button text for the translation modal.',
|
|
||||||
},
|
|
||||||
languageSelectionModalTitle: {
|
|
||||||
id: 'translationSelection.languageSelectionModalTitle',
|
|
||||||
defaultMessage: 'Translate this course',
|
|
||||||
description: 'Title for the translation modal.',
|
|
||||||
},
|
|
||||||
cancelButtonText: {
|
|
||||||
id: 'translationSelection.cancelButtonText',
|
|
||||||
defaultMessage: 'Cancel',
|
|
||||||
description: 'Button text for the translation modal.',
|
|
||||||
},
|
|
||||||
submitButtonText: {
|
|
||||||
id: 'translationSelection.submitButtonText',
|
|
||||||
defaultMessage: 'Submit',
|
|
||||||
description: 'Button text for the translation modal.',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
|
||||||
import {
|
|
||||||
getLocalStorage,
|
|
||||||
setLocalStorage,
|
|
||||||
} from '@src/data/localStorage';
|
|
||||||
|
|
||||||
export const selectedLanguageKey = 'selectedLanguages';
|
|
||||||
|
|
||||||
export const stateKeys = StrictDict({
|
|
||||||
selectedLanguage: 'selectedLanguage',
|
|
||||||
});
|
|
||||||
|
|
||||||
const useSelectLanguage = ({ courseId, language }) => {
|
|
||||||
const selectedLanguageItem = getLocalStorage(selectedLanguageKey) || {};
|
|
||||||
const [selectedLanguage, updateSelectedLanguage] = useKeyedState(
|
|
||||||
stateKeys.selectedLanguage,
|
|
||||||
selectedLanguageItem[courseId] || language,
|
|
||||||
);
|
|
||||||
|
|
||||||
const setSelectedLanguage = useCallback((newSelectedLanguage) => {
|
|
||||||
setLocalStorage(selectedLanguageKey, {
|
|
||||||
...selectedLanguageItem,
|
|
||||||
[courseId]: newSelectedLanguage,
|
|
||||||
});
|
|
||||||
updateSelectedLanguage(newSelectedLanguage);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedLanguage,
|
|
||||||
setSelectedLanguage,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSelectLanguage;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
|
||||||
import {
|
|
||||||
getLocalStorage,
|
|
||||||
setLocalStorage,
|
|
||||||
} from '@src/data/localStorage';
|
|
||||||
|
|
||||||
import useSelectLanguage, {
|
|
||||||
stateKeys,
|
|
||||||
selectedLanguageKey,
|
|
||||||
} from './useSelectLanguage';
|
|
||||||
|
|
||||||
const state = mockUseKeyedState(stateKeys);
|
|
||||||
|
|
||||||
jest.mock('react', () => ({
|
|
||||||
...jest.requireActual('react'),
|
|
||||||
useCallback: jest.fn((cb, prereqs) => (...args) => [
|
|
||||||
cb(...args),
|
|
||||||
{ cb, prereqs },
|
|
||||||
]),
|
|
||||||
}));
|
|
||||||
jest.mock('@src/data/localStorage', () => ({
|
|
||||||
getLocalStorage: jest.fn(),
|
|
||||||
setLocalStorage: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('useSelectLanguage', () => {
|
|
||||||
const props = {
|
|
||||||
courseId: 'test-course-id',
|
|
||||||
language: 'en',
|
|
||||||
};
|
|
||||||
const languages = [
|
|
||||||
{ code: 'en', label: 'English' },
|
|
||||||
{ code: 'es', label: 'Spanish' },
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
state.mock();
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
state.resetVals();
|
|
||||||
});
|
|
||||||
|
|
||||||
languages.forEach(({ code, label }) => {
|
|
||||||
it(`initializes selectedLanguage to the selected language (${label})`, () => {
|
|
||||||
getLocalStorage.mockReturnValueOnce({ [props.courseId]: code });
|
|
||||||
const { selectedLanguage } = useSelectLanguage(props);
|
|
||||||
|
|
||||||
state.expectInitializedWith(stateKeys.selectedLanguage, code);
|
|
||||||
expect(selectedLanguage).toBe(code);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setSelectedLanguage behavior', () => {
|
|
||||||
const { setSelectedLanguage } = useSelectLanguage(props);
|
|
||||||
|
|
||||||
setSelectedLanguage('es');
|
|
||||||
state.expectSetStateCalledWith(stateKeys.selectedLanguage, 'es');
|
|
||||||
expect(setLocalStorage).toHaveBeenCalledWith(selectedLanguageKey, {
|
|
||||||
[props.courseId]: 'es',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
export const stateKeys = StrictDict({
|
|
||||||
selectedIndex: 'selectedIndex',
|
|
||||||
});
|
|
||||||
|
|
||||||
const useTranslationModal = ({
|
|
||||||
selectedLanguage, setSelectedLanguage, close, availableLanguages,
|
|
||||||
}) => {
|
|
||||||
const [selectedIndex, setSelectedIndex] = useKeyedState(
|
|
||||||
stateKeys.selectedIndex,
|
|
||||||
availableLanguages.findIndex((lang) => lang.code === selectedLanguage),
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
|
||||||
const newSelectedLanguage = availableLanguages[selectedIndex].code;
|
|
||||||
setSelectedLanguage(newSelectedLanguage);
|
|
||||||
close();
|
|
||||||
}, [selectedIndex]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedIndex,
|
|
||||||
setSelectedIndex,
|
|
||||||
onSubmit,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useTranslationModal;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import useTranslationModal, { stateKeys } from './useTranslationModal';
|
|
||||||
|
|
||||||
const state = mockUseKeyedState(stateKeys);
|
|
||||||
|
|
||||||
jest.mock('react', () => ({
|
|
||||||
...jest.requireActual('react'),
|
|
||||||
useCallback: jest.fn((cb, prereqs) => (...args) => ([
|
|
||||||
cb(...args), { cb, prereqs },
|
|
||||||
])),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('useTranslationModal', () => {
|
|
||||||
const props = {
|
|
||||||
selectedLanguage: 'en',
|
|
||||||
setSelectedLanguage: jest.fn(),
|
|
||||||
close: jest.fn(),
|
|
||||||
availableLanguages: [
|
|
||||||
{ code: 'en', label: 'English' },
|
|
||||||
{ code: 'es', label: 'Spanish' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
state.mock();
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
state.resetVals();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('initializes selectedIndex to the index of the selected language', () => {
|
|
||||||
const { selectedIndex } = useTranslationModal(props);
|
|
||||||
|
|
||||||
state.expectInitializedWith(stateKeys.selectedIndex, 0);
|
|
||||||
expect(selectedIndex).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('onSubmit updates the selected language and closes the modal', () => {
|
|
||||||
const { onSubmit } = useTranslationModal({
|
|
||||||
...props,
|
|
||||||
selectedLanguage: 'es',
|
|
||||||
});
|
|
||||||
onSubmit();
|
|
||||||
expect(props.setSelectedLanguage).toHaveBeenCalledWith('es');
|
|
||||||
expect(props.close).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { useToggle } from '@openedx/paragon';
|
|
||||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const hasSeenTranslationTourKey = 'hasSeenTranslationTour';
|
|
||||||
|
|
||||||
export const stateKeys = StrictDict({
|
|
||||||
showTranslationTour: 'showTranslationTour',
|
|
||||||
});
|
|
||||||
|
|
||||||
const useTranslationTour = () => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
|
|
||||||
const [isTourEnabled, setIsTourEnabled] = useKeyedState(
|
|
||||||
stateKeys.showTranslationTour,
|
|
||||||
global.localStorage.getItem(hasSeenTranslationTourKey) !== 'true',
|
|
||||||
);
|
|
||||||
const [isOpen, open, close] = useToggle(false);
|
|
||||||
|
|
||||||
const endTour = useCallback(() => {
|
|
||||||
global.localStorage.setItem(hasSeenTranslationTourKey, 'true');
|
|
||||||
setIsTourEnabled(false);
|
|
||||||
}, [isTourEnabled, setIsTourEnabled]);
|
|
||||||
|
|
||||||
const tryIt = useCallback(() => {
|
|
||||||
endTour();
|
|
||||||
open();
|
|
||||||
}, [endTour, open]);
|
|
||||||
|
|
||||||
const translationTour = isTourEnabled
|
|
||||||
? {
|
|
||||||
tourId: 'translation',
|
|
||||||
enabled: isTourEnabled,
|
|
||||||
onDismiss: endTour,
|
|
||||||
onEnd: tryIt,
|
|
||||||
checkpoints: [
|
|
||||||
{
|
|
||||||
title: formatMessage(messages.translationTourModalTitle),
|
|
||||||
body: formatMessage(messages.translationTourModalBody),
|
|
||||||
placement: 'bottom',
|
|
||||||
target: '#translation-selection-button',
|
|
||||||
showDismissButton: true,
|
|
||||||
endButtonText: formatMessage(messages.tryItButtonText),
|
|
||||||
dismissButtonText: formatMessage(messages.dismissButtonText),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
translationTour,
|
|
||||||
isOpen,
|
|
||||||
open,
|
|
||||||
close,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useTranslationTour;
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
|
||||||
import { useToggle } from '@openedx/paragon';
|
|
||||||
|
|
||||||
import useTranslationTour, { stateKeys } from './useTranslationTour';
|
|
||||||
|
|
||||||
jest.mock('react', () => ({
|
|
||||||
...jest.requireActual('react'),
|
|
||||||
useCallback: jest.fn((cb, prereqs) => () => {
|
|
||||||
cb();
|
|
||||||
return { useCallback: { cb, prereqs } };
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
jest.mock('@openedx/paragon', () => ({
|
|
||||||
useToggle: jest.fn(),
|
|
||||||
}));
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
|
||||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
|
||||||
const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
|
|
||||||
// this provide consistent for the test on different platform/timezone
|
|
||||||
const formatDate = jest.fn(date => new Date(date).toISOString()).mockName('useIntl.formatDate');
|
|
||||||
return {
|
|
||||||
...i18n,
|
|
||||||
useIntl: jest.fn(() => ({
|
|
||||||
formatMessage,
|
|
||||||
formatDate,
|
|
||||||
})),
|
|
||||||
defineMessages: m => m,
|
|
||||||
FormattedMessage: () => 'FormattedMessage',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
jest.mock('@src/data/localStorage', () => ({
|
|
||||||
getLocalStorage: jest.fn(),
|
|
||||||
setLocalStorage: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const state = mockUseKeyedState(stateKeys);
|
|
||||||
|
|
||||||
describe('useTranslationSelection', () => {
|
|
||||||
const mockLocalStroage = {
|
|
||||||
getItem: jest.fn(),
|
|
||||||
setItem: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleOpen = jest.fn();
|
|
||||||
const toggleClose = jest.fn();
|
|
||||||
|
|
||||||
useToggle.mockReturnValue([false, toggleOpen, toggleClose]);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
state.mock();
|
|
||||||
window.localStorage = mockLocalStroage;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
state.resetVals();
|
|
||||||
delete window.localStorage;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('do not have translation tour if user already seen it', () => {
|
|
||||||
mockLocalStroage.getItem.mockReturnValueOnce('not seen');
|
|
||||||
const { translationTour } = useTranslationTour();
|
|
||||||
|
|
||||||
expect(translationTour.enabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('show translation tour if user has not seen it', () => {
|
|
||||||
mockLocalStroage.getItem.mockReturnValueOnce('true');
|
|
||||||
const { translationTour } = useTranslationTour();
|
|
||||||
|
|
||||||
expect(translationTour).toMatchObject({});
|
|
||||||
});
|
|
||||||
test('open and close as pass from useToggle', () => {
|
|
||||||
const { isOpen, open, close } = useTranslationTour();
|
|
||||||
expect(isOpen).toBe(false);
|
|
||||||
expect(toggleOpen).toBe(open);
|
|
||||||
expect(toggleClose).toBe(close);
|
|
||||||
});
|
|
||||||
test('end tour on dismiss button click', () => {
|
|
||||||
mockLocalStroage.getItem.mockReturnValueOnce('not seen');
|
|
||||||
const { translationTour } = useTranslationTour();
|
|
||||||
translationTour.onDismiss();
|
|
||||||
expect(mockLocalStroage.setItem).toHaveBeenCalledWith(
|
|
||||||
'hasSeenTranslationTour',
|
|
||||||
'true',
|
|
||||||
);
|
|
||||||
state.expectSetStateCalledWith(stateKeys.showTranslationTour, false);
|
|
||||||
});
|
|
||||||
test('end tour and open modal on try it button click', () => {
|
|
||||||
mockLocalStroage.getItem.mockReturnValueOnce('not seen');
|
|
||||||
const { translationTour } = useTranslationTour();
|
|
||||||
translationTour.onEnd();
|
|
||||||
state.expectSetStateCalledWith(stateKeys.showTranslationTour, false);
|
|
||||||
expect(toggleOpen).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
|
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
|
||||||
<script src="https://www.edx.org/optimizelyjs/<%= htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
export const DECODE_ROUTES = {
|
|
||||||
ACCESS_DENIED: '/course/:courseId/access-denied',
|
|
||||||
HOME: '/course/:courseId/home',
|
|
||||||
LIVE: '/course/:courseId/live',
|
|
||||||
DATES: '/course/:courseId/dates',
|
|
||||||
DISCUSSION: '/course/:courseId/discussion/:path/*',
|
|
||||||
PROGRESS: [
|
|
||||||
'/course/:courseId/progress/:targetUserId/',
|
|
||||||
'/course/:courseId/progress',
|
|
||||||
],
|
|
||||||
COURSE_END: '/course/:courseId/course-end',
|
|
||||||
COURSEWARE: [
|
|
||||||
'/course/:courseId/:sequenceId/:unitId',
|
|
||||||
'/course/:courseId/:sequenceId',
|
|
||||||
'/course/:courseId',
|
|
||||||
],
|
|
||||||
REDIRECT_HOME: 'home/:courseId',
|
|
||||||
REDIRECT_SURVEY: 'survey/:courseId',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ROUTES = {
|
|
||||||
UNSUBSCRIBE: '/goal-unsubscribe/:token',
|
|
||||||
REDIRECT: '/redirect/*',
|
|
||||||
DASHBOARD: 'dashboard',
|
|
||||||
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
|
|
||||||
CONSENT: 'consent',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const REDIRECT_MODES = {
|
|
||||||
DASHBOARD_REDIRECT: 'dashboard-redirect',
|
|
||||||
ENTERPRISE_LEARNER_DASHBOARD_REDIRECT: 'enterprise-learner-dashboard-redirect',
|
|
||||||
CONSENT_REDIRECT: 'consent-redirect',
|
|
||||||
HOME_REDIRECT: 'home-redirect',
|
|
||||||
SURVEY_REDIRECT: 'survey-redirect',
|
|
||||||
};
|
|
||||||
74
src/constants.ts
Normal file
74
src/constants.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
export const DECODE_ROUTES = {
|
||||||
|
ACCESS_DENIED: '/course/:courseId/access-denied',
|
||||||
|
HOME: '/course/:courseId/home',
|
||||||
|
LIVE: '/course/:courseId/live',
|
||||||
|
DATES: '/course/:courseId/dates',
|
||||||
|
DISCUSSION: '/course/:courseId/discussion/:path/*',
|
||||||
|
PROGRESS: [
|
||||||
|
'/course/:courseId/progress/:targetUserId/',
|
||||||
|
'/course/:courseId/progress',
|
||||||
|
],
|
||||||
|
COURSE_END: '/course/:courseId/course-end',
|
||||||
|
COURSEWARE: [
|
||||||
|
'/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',
|
||||||
|
ENTERPRISE_LEARNER_DASHBOARD_REDIRECT: 'enterprise-learner-dashboard-redirect',
|
||||||
|
CONSENT_REDIRECT: 'consent-redirect',
|
||||||
|
HOME_REDIRECT: 'home-redirect',
|
||||||
|
SURVEY_REDIRECT: 'survey-redirect',
|
||||||
|
} as const satisfies Readonly<{ [k: string]: string }>;
|
||||||
|
|
||||||
|
export const VERIFIED_MODES = [
|
||||||
|
'professional',
|
||||||
|
'verified',
|
||||||
|
'no-id-professional',
|
||||||
|
'credit',
|
||||||
|
'masters',
|
||||||
|
'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"
|
variant="tabs"
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onSelect={setFilter}
|
onSelect={setFilter}
|
||||||
|
aria-label={intl.formatMessage(messages.searchResultsFilterDescription)}
|
||||||
>
|
>
|
||||||
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
|
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
|
||||||
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}>
|
<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 { useParams } from 'react-router';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Alert, Button, Icon, Spinner,
|
Alert, Button, Icon, Spinner,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
@@ -18,7 +18,8 @@ import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter';
|
|||||||
import { updateModel, useModel } from '../../generic/model-store';
|
import { updateModel, useModel } from '../../generic/model-store';
|
||||||
import { searchCourseContent } from '../data/thunks';
|
import { searchCourseContent } from '../data/thunks';
|
||||||
|
|
||||||
const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
const CoursewareSearch = ({ ...sectionProps }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
|
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -29,6 +30,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
errors,
|
errors,
|
||||||
total,
|
total,
|
||||||
} = useModel('contentSearchResults', courseId);
|
} = useModel('contentSearchResults', courseId);
|
||||||
|
const dialogRef = useRef();
|
||||||
|
|
||||||
useLockScroll();
|
useLockScroll();
|
||||||
|
|
||||||
@@ -44,7 +46,8 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
results: [],
|
results: [],
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
loading: false,
|
loading:
|
||||||
|
false,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -66,20 +69,46 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
setQuery(value);
|
setQuery(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleSubmit(searchKeyword);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnChange = (value) => {
|
const handleOnChange = (value) => {
|
||||||
if (value === searchKeyword) { return; }
|
if (value === searchKeyword) { return; }
|
||||||
if (!value) { clearSearch(); }
|
if (!value) { clearSearch(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchCloseClick = () => {
|
const close = () => {
|
||||||
clearSearch();
|
clearSearch();
|
||||||
dispatch(setShowSearch(false));
|
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';
|
let status = 'idle';
|
||||||
if (loading) {
|
if (loading) {
|
||||||
status = 'loading';
|
status = 'loading';
|
||||||
@@ -90,59 +119,64 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-section" {...sectionProps}>
|
<dialog ref={dialogRef} className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-dialog" onClose={handleSearchClose} {...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>
|
|
||||||
<div className="courseware-search__outer-content">
|
<div className="courseware-search__outer-content">
|
||||||
<div className="courseware-search__content">
|
<div className="courseware-search__content" data-testid="courseware-search-content">
|
||||||
<h1 className="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1>
|
<div className="courseware-search__form">
|
||||||
<CoursewareSearchForm
|
<h1 className="h2">{formatMessage(messages.searchModuleTitle)}</h1>
|
||||||
searchTerm={searchKeyword}
|
<CoursewareSearchForm
|
||||||
onSubmit={handleSubmit}
|
searchTerm={searchKeyword}
|
||||||
onChange={handleOnChange}
|
onSubmit={handleSubmit}
|
||||||
placeholder={intl.formatMessage(messages.searchBarPlaceholderText)}
|
onChange={handleOnChange}
|
||||||
/>
|
placeholder={formatMessage(messages.searchBarPlaceholderText)}
|
||||||
{status === 'loading' ? (
|
/>
|
||||||
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
<div className="courseware-search__close">
|
||||||
<Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} />
|
<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>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
{status === 'error' && (
|
<div
|
||||||
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
key={status}
|
||||||
{intl.formatMessage(messages.searchResultsError)}
|
className="courseware-search__results"
|
||||||
</Alert>
|
aria-live="assertive"
|
||||||
)}
|
aria-atomic="true"
|
||||||
{status === 'results' ? (
|
aria-busy={status === 'loading'}
|
||||||
<>
|
data-testid="courseware-search-results"
|
||||||
{total > 0 ? (
|
role={status === 'results' ? 'alert' : 'none'}
|
||||||
<div
|
>
|
||||||
className="courseware-search__results-summary"
|
{status === 'loading' ? (
|
||||||
aria-live="polite"
|
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
||||||
aria-relevant="all"
|
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
|
||||||
aria-atomic="true"
|
</div>
|
||||||
data-testid="courseware-search-summary"
|
) : null}
|
||||||
>{intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
{status === 'error' && (
|
||||||
</div>
|
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
||||||
) : null}
|
{formatMessage(messages.searchResultsError)}
|
||||||
<CoursewareSearchResultsFilterContainer />
|
</Alert>
|
||||||
</>
|
)}
|
||||||
) : null}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CoursewareSearch.propTypes = {
|
export default CoursewareSearch;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CoursewareSearch);
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
screen,
|
screen,
|
||||||
waitFor,
|
waitFor,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
|
within,
|
||||||
} from '../../setupTest';
|
} from '../../setupTest';
|
||||||
import { CoursewareSearch } from './index';
|
import { CoursewareSearch } from './index';
|
||||||
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks';
|
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks';
|
||||||
@@ -19,6 +20,7 @@ import { updateModel, useModel } from '../../generic/model-store';
|
|||||||
|
|
||||||
jest.mock('./hooks');
|
jest.mock('./hooks');
|
||||||
jest.mock('../../generic/model-store', () => ({
|
jest.mock('../../generic/model-store', () => ({
|
||||||
|
...jest.requireActual('../../generic/model-store'),
|
||||||
updateModel: jest.fn(),
|
updateModel: jest.fn(),
|
||||||
useModel: jest.fn(),
|
useModel: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -56,7 +58,7 @@ const defaultProps = {
|
|||||||
total: 0,
|
total: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const coursewareSearch = {
|
const defaultSearchParams = {
|
||||||
query: '',
|
query: '',
|
||||||
filter: '',
|
filter: '',
|
||||||
setQuery: jest.fn(),
|
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);
|
useCoursewareSearchParams.mockReturnValue(props);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CoursewareSearch', () => {
|
describe('CoursewareSearch', () => {
|
||||||
beforeAll(initializeMockApp);
|
beforeAll(() => initializeMockApp());
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockModels();
|
||||||
|
mockSearchParams();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,27 +121,22 @@ describe('CoursewareSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
|
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
expect(useElementBoundingBox).toBeCalledTimes(1);
|
expect(useElementBoundingBox).toHaveBeenCalledTimes(1);
|
||||||
expect(useLockScroll).toBeCalledTimes(1);
|
expect(useLockScroll).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
|
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent();
|
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`);
|
expect(section.style.getPropertyValue('--modal-top-position')).toBe(`${tabsTopPosition}px`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when clicking on the "Close" button', () => {
|
describe('when clicking on the "Close" button', () => {
|
||||||
it('should dispatch setShowSearch(false)', async () => {
|
it('should close the dialog', async () => {
|
||||||
mockModels();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -141,7 +144,8 @@ describe('CoursewareSearch', () => {
|
|||||||
fireEvent.click(close);
|
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', () => {
|
it('should use "--modal-top-position: 0" if nce element is not present', () => {
|
||||||
useElementBoundingBox.mockImplementation(() => undefined);
|
useElementBoundingBox.mockImplementation(() => undefined);
|
||||||
|
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
const section = screen.getByTestId('courseware-search-section');
|
const section = screen.getByTestId('courseware-search-dialog');
|
||||||
expect(section.style.getPropertyValue('--modal-top-position')).toBe('0');
|
expect(section.style.getPropertyValue('--modal-top-position')).toBe('0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when passing extra props', () => {
|
describe('when passing extra props', () => {
|
||||||
it('should pass on extra props to section element', () => {
|
it('should pass on extra props to section element', () => {
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent({ foo: 'bar' });
|
renderComponent({ foo: 'bar' });
|
||||||
|
|
||||||
const section = screen.getByTestId('courseware-search-section');
|
const section = screen.getByTestId('courseware-search-dialog');
|
||||||
expect(section).toHaveAttribute('foo', 'bar');
|
expect(section).toHaveAttribute('foo', 'bar');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when submitting an empty search', () => {
|
describe('when submitting an empty search', () => {
|
||||||
it('should clear the search by dispatch updateModel', async () => {
|
it('should clear the search by dispatch updateModel', async () => {
|
||||||
mockModels();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -203,7 +202,6 @@ describe('CoursewareSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchCourseContent', async () => {
|
it('should call searchCourseContent', async () => {
|
||||||
mockModels();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
const searchKeyword = 'course';
|
const searchKeyword = 'course';
|
||||||
@@ -246,19 +244,25 @@ describe('CoursewareSearch', () => {
|
|||||||
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
|
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({
|
mockModels({
|
||||||
searchKeyword: 'fubar',
|
searchKeyword: 'fubar',
|
||||||
total: 1,
|
total: 1,
|
||||||
});
|
});
|
||||||
renderComponent();
|
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', () => {
|
describe('when clearing the search input', () => {
|
||||||
it('should clear the search by dispatch updateModel', async () => {
|
it('should clear the search by dispatch updateModel', async () => {
|
||||||
|
mockSearchParams({ query: 'fubar' });
|
||||||
mockModels({
|
mockModels({
|
||||||
searchKeyword: 'fubar',
|
searchKeyword: 'fubar',
|
||||||
total: 2,
|
total: 2,
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SearchField } from '@openedx/paragon';
|
import { SearchField } from '@openedx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CoursewareSearchForm = ({
|
const CoursewareSearchForm = ({
|
||||||
intl,
|
|
||||||
searchTerm,
|
searchTerm,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
}) => (
|
}) => {
|
||||||
<SearchField.Advanced
|
const { formatMessage } = useIntl();
|
||||||
value={searchTerm}
|
|
||||||
onSubmit={onSubmit}
|
return (
|
||||||
onChange={onChange}
|
<SearchField.Advanced
|
||||||
submitButtonLocation="external"
|
value={searchTerm}
|
||||||
className="courseware-search-form"
|
onSubmit={onSubmit}
|
||||||
screenReaderText={{
|
onChange={onChange}
|
||||||
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)}
|
|
||||||
submitButtonLocation="external"
|
submitButtonLocation="external"
|
||||||
data-testid="courseware-search-form-submit"
|
className="courseware-search-form"
|
||||||
/>
|
screenReaderText={{
|
||||||
</SearchField.Advanced>
|
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 = {
|
CoursewareSearchForm.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
searchTerm: PropTypes.string,
|
searchTerm: PropTypes.string,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
@@ -51,4 +52,4 @@ CoursewareSearchForm.defaultProps = {
|
|||||||
placeholder: undefined,
|
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 {
|
import {
|
||||||
Folder, TextFields, VideoCamera, Article,
|
Folder, TextFields, VideoCamera, Article,
|
||||||
} from '@openedx/paragon/icons';
|
} from '@openedx/paragon/icons';
|
||||||
@@ -6,6 +6,7 @@ import { getConfig } from '@edx/frontend-platform';
|
|||||||
import { Icon } from '@openedx/paragon';
|
import { Icon } from '@openedx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
const iconTypeMapping = {
|
const iconTypeMapping = {
|
||||||
text: TextFields,
|
text: TextFields,
|
||||||
@@ -21,6 +22,8 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
|||||||
return <CoursewareSearchEmpty />;
|
return <CoursewareSearchEmpty />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const baseUrl = `${getConfig().LMS_BASE_URL}`;
|
const baseUrl = `${getConfig().LMS_BASE_URL}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,24 +45,30 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
|||||||
rel: 'nofollow',
|
rel: 'nofollow',
|
||||||
} : { href: `${baseUrl}${url}` };
|
} : { href: `${baseUrl}${url}` };
|
||||||
|
|
||||||
|
const ariaSeaparator = formatMessage(messages.searchResultsBreadcrumbSeparator);
|
||||||
|
const ariaLocation = location?.length ? formatMessage(messages.searchResultsBreadcrumb, { path: location.join(ariaSeaparator) }) : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a key={id} className="courseware-search-results__item" {...linkProps}>
|
<a key={id} className="courseware-search-results__item" {...linkProps}>
|
||||||
<div className="courseware-search-results__icon"><Icon src={icon} /></div>
|
<div className="courseware-search-results__icon"><Icon src={icon} /></div>
|
||||||
<div className="courseware-search-results__info">
|
<div className="courseware-search-results__info">
|
||||||
<div className="courseware-search-results__title">
|
<div className="courseware-search-results__title">
|
||||||
<span>{title}</span>
|
<h3>{title}</h3>
|
||||||
{contentHits ? (<em>{contentHits}</em>) : null }
|
{contentHits ? (<em aria-hidden="true">{contentHits}</em>) : null }
|
||||||
</div>
|
</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.
|
// This ignore is necessary because the breadcrumb texts might have duplicates.
|
||||||
// The breadcrumbs are not expected to change.
|
// The breadcrumbs are not expected to change.
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
location.map((breadcrumb, i) => (<li key={`${i}:${breadcrumb}`}><div>{breadcrumb}</div></li>))
|
location.map((breadcrumb, i) => (<li key={`${i}:${breadcrumb}`}><div>{breadcrumb}</div></li>))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, Icon } from '@openedx/paragon';
|
import { Button } from '@openedx/paragon';
|
||||||
import { Search } from '@openedx/paragon/icons';
|
import { ManageSearch } from '@openedx/paragon/icons';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
|
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
|
||||||
@@ -25,16 +25,17 @@ const CoursewareSearchToggle = ({
|
|||||||
if (!enabled) { return null; }
|
if (!enabled) { return null; }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="courseware-searc-toggle">
|
<div className="courseware-search-toggle">
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="outline-primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="p-1 mt-2 mr-2 rounded-lg"
|
className="p-1 mt-2 mr-2"
|
||||||
aria-label={intl.formatMessage(messages.searchOpenAction)}
|
aria-label={intl.formatMessage(messages.searchOpenAction)}
|
||||||
onClick={handleSearchOpenClick}
|
onClick={handleSearchOpenClick}
|
||||||
data-testid="courseware-search-open-button"
|
data-testid="courseware-search-open-button"
|
||||||
|
iconAfter={ManageSearch}
|
||||||
>
|
>
|
||||||
<Icon src={Search} />
|
{intl.formatMessage(messages.contentSearchButton)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,24 +38,29 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Demo Course Overview
|
Demo Course Overview
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Introduction, then Demo Course Overview."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Introduction
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Introduction
|
||||||
Demo Course Overview
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Demo Course Overview
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -91,32 +96,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Passing a Course
|
Passing a Course
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
1
|
1
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
About Exams and Certificates
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
About Exams and Certificates
|
||||||
edX Exams
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
edX Exams
|
||||||
Passing a Course
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Passing a Course
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -152,29 +164,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Passing a Course
|
Passing a Course
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
About Exams and Certificates
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
About Exams and Certificates
|
||||||
edX Exams
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
edX Exams
|
||||||
Passing a Course
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Passing a Course
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -210,29 +227,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Text Input
|
Text Input
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Text input."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 1: Getting Started
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 1: Getting Started
|
||||||
Homework - Question Styles
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Homework - Question Styles
|
||||||
Text input
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Text input
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -268,29 +290,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Pointing on a Picture
|
Pointing on a Picture
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Pointing on a Picture."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 1: Getting Started
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 1: Getting Started
|
||||||
Homework - Question Styles
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Homework - Question Styles
|
||||||
Pointing on a Picture
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Pointing on a Picture
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -326,29 +353,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Getting Answers
|
Getting Answers
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: About Exams and Certificates, then edX Exams, then Getting Answers."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
About Exams and Certificates
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
About Exams and Certificates
|
||||||
edX Exams
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
edX Exams
|
||||||
Getting Answers
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Getting Answers
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -384,32 +416,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Welcome!
|
Welcome!
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
30
|
30
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Introduction
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Introduction
|
||||||
Demo Course Overview
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Demo Course Overview
|
||||||
Introduction: Video and Sequences
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Introduction: Video and Sequences
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -445,29 +484,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Multiple Choice Questions
|
Multiple Choice Questions
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Multiple Choice Questions."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 1: Getting Started
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 1: Getting Started
|
||||||
Homework - Question Styles
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Homework - Question Styles
|
||||||
Multiple Choice Questions
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Multiple Choice Questions
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -503,29 +547,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Numerical Input
|
Numerical Input
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Numerical Input."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 1: Getting Started
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 1: Getting Started
|
||||||
Homework - Question Styles
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Homework - Question Styles
|
||||||
Numerical Input
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Numerical Input
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -561,32 +610,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Connecting a Circuit and a Circuit Diagram
|
Connecting a Circuit and a Circuit Diagram
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
3
|
3
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Video Presentation Styles."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 1: Getting Started
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 1: Getting Started
|
||||||
Lesson 1 - Getting Started
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Lesson 1 - Getting Started
|
||||||
Video Presentation Styles
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Video Presentation Styles
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -622,29 +678,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
CAPA
|
CAPA
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 2: Get Interactive, then Homework - Labs and Demos, then Code Grader."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 2: Get Interactive
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 2: Get Interactive
|
||||||
Homework - Labs and Demos
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Homework - Labs and Demos
|
||||||
Code Grader
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Code Grader
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -680,29 +741,34 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Interactive Questions
|
Interactive Questions
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Interactive Questions."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 1: Getting Started
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 1: Getting Started
|
||||||
Lesson 1 - Getting Started
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Lesson 1 - Getting Started
|
||||||
Interactive Questions
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Interactive Questions
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -738,32 +804,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Blank HTML Page
|
Blank HTML Page
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
6
|
6
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Introduction
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Introduction
|
||||||
Demo Course Overview
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Demo Course Overview
|
||||||
Introduction: Video and Sequences
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Introduction: Video and Sequences
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -799,32 +872,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Discussion Forums
|
Discussion Forums
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
5
|
5
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Discussion Forums."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 3: Be Social
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 3: Be Social
|
||||||
Lesson 3 - Be Social
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Lesson 3 - Be Social
|
||||||
Discussion Forums
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Discussion Forums
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -860,32 +940,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Overall Grade
|
Overall Grade
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
7
|
7
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: About Exams and Certificates, then edX Exams, then Overall Grade Performance."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
About Exams and Certificates
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
About Exams and Certificates
|
||||||
edX Exams
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
edX Exams
|
||||||
Overall Grade Performance
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Overall Grade Performance
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -921,32 +1008,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Blank HTML Page
|
Blank HTML Page
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
3
|
3
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Homework - Find Your Study Buddy."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 3: Be Social
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 3: Be Social
|
||||||
Lesson 3 - Be Social
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Lesson 3 - Be Social
|
||||||
Homework - Find Your Study Buddy
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Homework - Find Your Study Buddy
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -982,32 +1076,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Find Your Study Buddy
|
Find Your Study Buddy
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
3
|
3
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 3: Be Social, then Homework - Find Your Study Buddy, then Homework - Find Your Study Buddy."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 3: Be Social
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 3: Be Social
|
||||||
Homework - Find Your Study Buddy
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Homework - Find Your Study Buddy
|
||||||
Homework - Find Your Study Buddy
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Homework - Find Your Study Buddy
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -1043,32 +1144,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
Be Social
|
Be Social
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
4
|
4
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Be Social."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 3: Be Social
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 3: Be Social
|
||||||
Lesson 3 - Be Social
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Lesson 3 - Be Social
|
||||||
Be Social
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -1104,32 +1212,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
EdX Exams
|
EdX Exams
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
4
|
4
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: About Exams and Certificates, then edX Exams, then EdX Exams."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
About Exams and Certificates
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
About Exams and Certificates
|
||||||
edX Exams
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
edX Exams
|
||||||
EdX Exams
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
EdX Exams
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -1165,32 +1280,39 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
When Are Your Exams?
|
When Are Your Exams?
|
||||||
</span>
|
</h3>
|
||||||
<em>
|
<em
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
2
|
2
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<div
|
||||||
class="courseware-search-results__breadcrumbs"
|
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then When Are Your Exams? ."
|
||||||
>
|
>
|
||||||
<li>
|
<ul
|
||||||
<div>
|
aria-hidden="true"
|
||||||
Example Week 1: Getting Started
|
class="courseware-search-results__breadcrumbs"
|
||||||
</div>
|
>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Example Week 1: Getting Started
|
||||||
Lesson 1 - Getting Started
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div>
|
||||||
<div>
|
Lesson 1 - Getting Started
|
||||||
When Are Your Exams?
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<div>
|
||||||
|
When Are Your Exams?
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -1228,10 +1350,13 @@ exports[`CoursewareSearchResults when list of results is provided should match t
|
|||||||
<div
|
<div
|
||||||
class="courseware-search-results__title"
|
class="courseware-search-results__title"
|
||||||
>
|
>
|
||||||
<span>
|
<h3>
|
||||||
External Course Link Test
|
External Course Link Test
|
||||||
</span>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
|
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
|
||||||
Object {
|
{
|
||||||
"filters": Array [
|
"filters": [
|
||||||
Object {
|
{
|
||||||
"count": 7,
|
"count": 7,
|
||||||
"key": "capa",
|
"key": "capa",
|
||||||
"label": "CAPA",
|
"label": "CAPA",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"key": "sequence",
|
"key": "sequence",
|
||||||
"label": "Sequence",
|
"label": "Sequence",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"count": 9,
|
"count": 9,
|
||||||
"key": "text",
|
"key": "text",
|
||||||
"label": "Text",
|
"label": "Text",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"key": "unknown",
|
"key": "unknown",
|
||||||
"label": "Unknown",
|
"label": "Unknown",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"key": "video",
|
"key": "video",
|
||||||
"label": "Video",
|
"label": "Video",
|
||||||
@@ -31,11 +31,11 @@ Object {
|
|||||||
],
|
],
|
||||||
"maxScore": 3.4545178,
|
"maxScore": 3.4545178,
|
||||||
"ms": 5,
|
"ms": 5,
|
||||||
"results": Array [
|
"results": [
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Introduction",
|
"Introduction",
|
||||||
"Demo Course Overview",
|
"Demo Course Overview",
|
||||||
],
|
],
|
||||||
@@ -44,10 +44,10 @@ Object {
|
|||||||
"type": "sequence",
|
"type": "sequence",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||||
"location": Array [
|
"location": [
|
||||||
"About Exams and Certificates",
|
"About Exams and Certificates",
|
||||||
"edX Exams",
|
"edX Exams",
|
||||||
"Passing a Course",
|
"Passing a Course",
|
||||||
@@ -57,10 +57,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||||
"location": Array [
|
"location": [
|
||||||
"About Exams and Certificates",
|
"About Exams and Certificates",
|
||||||
"edX Exams",
|
"edX Exams",
|
||||||
"Passing a Course",
|
"Passing a Course",
|
||||||
@@ -70,10 +70,10 @@ Object {
|
|||||||
"type": "sequence",
|
"type": "sequence",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 1: Getting Started",
|
"Example Week 1: Getting Started",
|
||||||
"Homework - Question Styles",
|
"Homework - Question Styles",
|
||||||
"Text input",
|
"Text input",
|
||||||
@@ -83,10 +83,10 @@ Object {
|
|||||||
"type": "capa",
|
"type": "capa",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 1: Getting Started",
|
"Example Week 1: Getting Started",
|
||||||
"Homework - Question Styles",
|
"Homework - Question Styles",
|
||||||
"Pointing on a Picture",
|
"Pointing on a Picture",
|
||||||
@@ -96,10 +96,10 @@ Object {
|
|||||||
"type": "capa",
|
"type": "capa",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||||
"location": Array [
|
"location": [
|
||||||
"About Exams and Certificates",
|
"About Exams and Certificates",
|
||||||
"edX Exams",
|
"edX Exams",
|
||||||
"Getting Answers",
|
"Getting Answers",
|
||||||
@@ -109,10 +109,10 @@ Object {
|
|||||||
"type": "capa",
|
"type": "capa",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Introduction",
|
"Introduction",
|
||||||
"Demo Course Overview",
|
"Demo Course Overview",
|
||||||
"Introduction: Video and Sequences",
|
"Introduction: Video and Sequences",
|
||||||
@@ -122,10 +122,10 @@ Object {
|
|||||||
"type": "video",
|
"type": "video",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 1: Getting Started",
|
"Example Week 1: Getting Started",
|
||||||
"Homework - Question Styles",
|
"Homework - Question Styles",
|
||||||
"Multiple Choice Questions",
|
"Multiple Choice Questions",
|
||||||
@@ -135,10 +135,10 @@ Object {
|
|||||||
"type": "capa",
|
"type": "capa",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 1: Getting Started",
|
"Example Week 1: Getting Started",
|
||||||
"Homework - Question Styles",
|
"Homework - Question Styles",
|
||||||
"Numerical Input",
|
"Numerical Input",
|
||||||
@@ -148,10 +148,10 @@ Object {
|
|||||||
"type": "capa",
|
"type": "capa",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 1: Getting Started",
|
"Example Week 1: Getting Started",
|
||||||
"Lesson 1 - Getting Started",
|
"Lesson 1 - Getting Started",
|
||||||
"Video Presentation Styles",
|
"Video Presentation Styles",
|
||||||
@@ -161,10 +161,10 @@ Object {
|
|||||||
"type": "video",
|
"type": "video",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 2: Get Interactive",
|
"Example Week 2: Get Interactive",
|
||||||
"Homework - Labs and Demos",
|
"Homework - Labs and Demos",
|
||||||
"Code Grader",
|
"Code Grader",
|
||||||
@@ -174,10 +174,10 @@ Object {
|
|||||||
"type": "capa",
|
"type": "capa",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 1: Getting Started",
|
"Example Week 1: Getting Started",
|
||||||
"Lesson 1 - Getting Started",
|
"Lesson 1 - Getting Started",
|
||||||
"Interactive Questions",
|
"Interactive Questions",
|
||||||
@@ -187,10 +187,10 @@ Object {
|
|||||||
"type": "capa",
|
"type": "capa",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Introduction",
|
"Introduction",
|
||||||
"Demo Course Overview",
|
"Demo Course Overview",
|
||||||
"Introduction: Video and Sequences",
|
"Introduction: Video and Sequences",
|
||||||
@@ -200,10 +200,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 3: Be Social",
|
"Example Week 3: Be Social",
|
||||||
"Lesson 3 - Be Social",
|
"Lesson 3 - Be Social",
|
||||||
"Discussion Forums",
|
"Discussion Forums",
|
||||||
@@ -213,10 +213,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||||
"location": Array [
|
"location": [
|
||||||
"About Exams and Certificates",
|
"About Exams and Certificates",
|
||||||
"edX Exams",
|
"edX Exams",
|
||||||
"Overall Grade Performance",
|
"Overall Grade Performance",
|
||||||
@@ -226,10 +226,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 3: Be Social",
|
"Example Week 3: Be Social",
|
||||||
"Lesson 3 - Be Social",
|
"Lesson 3 - Be Social",
|
||||||
"Homework - Find Your Study Buddy",
|
"Homework - Find Your Study Buddy",
|
||||||
@@ -239,10 +239,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 3: Be Social",
|
"Example Week 3: Be Social",
|
||||||
"Homework - Find Your Study Buddy",
|
"Homework - Find Your Study Buddy",
|
||||||
"Homework - Find Your Study Buddy",
|
"Homework - Find Your Study Buddy",
|
||||||
@@ -252,10 +252,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 3: Be Social",
|
"Example Week 3: Be Social",
|
||||||
"Lesson 3 - Be Social",
|
"Lesson 3 - Be Social",
|
||||||
"Be Social",
|
"Be Social",
|
||||||
@@ -265,10 +265,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||||
"location": Array [
|
"location": [
|
||||||
"About Exams and Certificates",
|
"About Exams and Certificates",
|
||||||
"edX Exams",
|
"edX Exams",
|
||||||
"EdX Exams",
|
"EdX Exams",
|
||||||
@@ -278,10 +278,10 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||||
"location": Array [
|
"location": [
|
||||||
"Example Week 1: Getting Started",
|
"Example Week 1: Getting Started",
|
||||||
"Lesson 1 - Getting Started",
|
"Lesson 1 - Getting Started",
|
||||||
"When Are Your Exams? ",
|
"When Are Your Exams? ",
|
||||||
@@ -291,7 +291,7 @@ Object {
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"contentHits": 0,
|
"contentHits": 0,
|
||||||
"id": "random-element-id",
|
"id": "random-element-id",
|
||||||
"location": null,
|
"location": null,
|
||||||
|
|||||||
@@ -5,13 +5,25 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
border-top: 1px solid $light-300;
|
border-top: 1px solid $light-300;
|
||||||
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
|
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.h2 {
|
||||||
|
margin-right: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__close {
|
&__close {
|
||||||
position: absolute !important; // For some reason it gets overridden
|
position: absolute !important; // For some reason it gets overridden
|
||||||
top: 0.5rem;
|
top: 0;
|
||||||
right: 1rem;
|
right: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -89,6 +101,11 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: $black;
|
color: $black;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -152,9 +169,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, 'md')) {
|
@media (min-width: map-get($grid-breakpoints, 'md')) {
|
||||||
.courseware-search__content {
|
.courseware-search {
|
||||||
padding-top: 8rem;
|
&__close {
|
||||||
|
right: -2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
padding-top: 8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body._search-no-scroll {
|
body._search-no-scroll {
|
||||||
|
|||||||
@@ -2,79 +2,98 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
searchOpenAction: {
|
searchOpenAction: {
|
||||||
id: 'learn.coursewareSerch.openAction',
|
id: 'learn.coursewareSearch.openAction',
|
||||||
defaultMessage: 'Search within this course',
|
defaultMessage: 'Search within this course',
|
||||||
description: 'Aria-label for a button that will pop up Courseware Search.',
|
description: 'Aria-label for a button that will pop up Courseware Search.',
|
||||||
},
|
},
|
||||||
|
contentSearchButton: {
|
||||||
|
id: 'learn.coursewareSearch.contentSearchButton',
|
||||||
|
defaultMessage: 'Content search',
|
||||||
|
description: 'Text for a button that will pop up Courseware Search.',
|
||||||
|
},
|
||||||
searchSubmitLabel: {
|
searchSubmitLabel: {
|
||||||
id: 'learn.coursewareSerch.submitLabel',
|
id: 'learn.coursewareSearch.submitLabel',
|
||||||
defaultMessage: 'Search',
|
defaultMessage: 'Search',
|
||||||
description: 'Button label that will submit Courseware Search.',
|
description: 'Button label that will submit Courseware Search.',
|
||||||
},
|
},
|
||||||
searchClearAction: {
|
searchClearAction: {
|
||||||
id: 'learn.coursewareSerch.clearAction',
|
id: 'learn.coursewareSearch.clearAction',
|
||||||
defaultMessage: 'Clear search',
|
defaultMessage: 'Clear search',
|
||||||
description: 'Button label that will the current Courseware Search input.',
|
description: 'Button label that will the current Courseware Search input.',
|
||||||
},
|
},
|
||||||
searchCloseAction: {
|
searchCloseAction: {
|
||||||
id: 'learn.coursewareSerch.closeAction',
|
id: 'learn.coursewareSearch.closeAction',
|
||||||
defaultMessage: 'Close the search form',
|
defaultMessage: 'Close the search form',
|
||||||
description: 'Aria-label for a button that will close Courseware Search.',
|
description: 'Aria-label for a button that will close Courseware Search.',
|
||||||
},
|
},
|
||||||
searchModuleTitle: {
|
searchModuleTitle: {
|
||||||
id: 'learn.coursewareSerch.searchModuleTitle',
|
id: 'learn.coursewareSearch.searchModuleTitle',
|
||||||
defaultMessage: 'Search this course',
|
defaultMessage: 'Search this course',
|
||||||
description: 'Title for the Courseware Search module.',
|
description: 'Title for the Courseware Search module.',
|
||||||
},
|
},
|
||||||
searchBarPlaceholderText: {
|
searchBarPlaceholderText: {
|
||||||
id: 'learn.coursewareSerch.searchBarPlaceholderText',
|
id: 'learn.coursewareSearch.searchBarPlaceholderText',
|
||||||
defaultMessage: 'Search',
|
defaultMessage: 'Search',
|
||||||
description: 'Placeholder text for the Courseware Search input control',
|
description: 'Placeholder text for the Courseware Search input control',
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
id: 'learn.coursewareSerch.loading',
|
id: 'learn.coursewareSearch.loading',
|
||||||
defaultMessage: 'Searching...',
|
defaultMessage: 'Searching...',
|
||||||
description: 'Screen reader text to use on the spinner while the search is performing.',
|
description: 'Screen reader text to use on the spinner while the search is performing.',
|
||||||
},
|
},
|
||||||
searchResultsNone: {
|
searchResultsNone: {
|
||||||
id: 'learn.coursewareSerch.searchResultsNone',
|
id: 'learn.coursewareSearch.searchResultsNone',
|
||||||
defaultMessage: 'No results found.',
|
defaultMessage: 'No results found.',
|
||||||
description: 'Text to show when the Courseware Search found no results matching the criteria.',
|
description: 'Text to show when the Courseware Search found no results matching the criteria.',
|
||||||
},
|
},
|
||||||
searchResultsLabel: {
|
searchResultsLabel: {
|
||||||
id: 'learn.coursewareSerch.searchResultsLabel',
|
id: 'learn.coursewareSearch.searchResultsLabel',
|
||||||
defaultMessage: 'Results for "{keyword}":',
|
defaultMessage: 'Results for "{keyword}":',
|
||||||
description: 'Text to show above the search results response list.',
|
description: 'Text to show above the search results response list.',
|
||||||
},
|
},
|
||||||
searchResultsError: {
|
searchResultsError: {
|
||||||
id: 'learn.coursewareSerch.searchResultsError',
|
id: 'learn.coursewareSearch.searchResultsError',
|
||||||
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.',
|
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.',
|
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
|
// These are translations for labeling the filters
|
||||||
'filter:all': {
|
'filter:all': {
|
||||||
id: 'learn.coursewareSerch.filter:all',
|
id: 'learn.coursewareSearch.filter:all',
|
||||||
defaultMessage: 'All content',
|
defaultMessage: 'All content',
|
||||||
description: 'Label for the search results filter that shows all content (no filter).',
|
description: 'Label for the search results filter that shows all content (no filter).',
|
||||||
},
|
},
|
||||||
'filter:text': {
|
'filter:text': {
|
||||||
id: 'learn.coursewareSerch.filter:text',
|
id: 'learn.coursewareSearch.filter:text',
|
||||||
defaultMessage: 'Text',
|
defaultMessage: 'Text',
|
||||||
description: 'Label for the search results filter that shows results with text content.',
|
description: 'Label for the search results filter that shows results with text content.',
|
||||||
},
|
},
|
||||||
'filter:video': {
|
'filter:video': {
|
||||||
id: 'learn.coursewareSerch.filter:video',
|
id: 'learn.coursewareSearch.filter:video',
|
||||||
defaultMessage: 'Video',
|
defaultMessage: 'Video',
|
||||||
description: 'Label for the search results filter that shows results with video content.',
|
description: 'Label for the search results filter that shows results with video content.',
|
||||||
},
|
},
|
||||||
'filter:sequence': {
|
'filter:sequence': {
|
||||||
id: 'learn.coursewareSerch.filter:sequence',
|
id: 'learn.coursewareSearch.filter:sequence',
|
||||||
defaultMessage: 'Section',
|
defaultMessage: 'Section',
|
||||||
description: 'Label for the search results filter that shows results with section content.',
|
description: 'Label for the search results filter that shows results with section content.',
|
||||||
},
|
},
|
||||||
'filter:other': {
|
'filter:other': {
|
||||||
id: 'learn.coursewareSerch.filter:other',
|
id: 'learn.coursewareSearch.filter:other',
|
||||||
defaultMessage: 'Other',
|
defaultMessage: 'Other',
|
||||||
description: 'Label for the search results filter that shows results with other content.',
|
description: 'Label for the search results filter that shows results with other content.',
|
||||||
},
|
},
|
||||||
@@ -6,6 +6,7 @@ Factory.define('courseHomeMetadata')
|
|||||||
.option('host', 'http://localhost:18000')
|
.option('host', 'http://localhost:18000')
|
||||||
.attrs({
|
.attrs({
|
||||||
title: 'Demonstration Course',
|
title: 'Demonstration Course',
|
||||||
|
is_new_discussion_sidebar_view_enabled: false,
|
||||||
is_self_paced: false,
|
is_self_paced: false,
|
||||||
is_enrolled: false,
|
is_enrolled: false,
|
||||||
is_staff: false,
|
is_staff: false,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
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",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
"proctoringPanelStatus": "loading",
|
"proctoringPanelStatus": "loading",
|
||||||
@@ -12,9 +12,13 @@ Object {
|
|||||||
"toastBodyText": null,
|
"toastBodyText": null,
|
||||||
"toastHeader": "",
|
"toastHeader": "",
|
||||||
},
|
},
|
||||||
"courseware": Object {
|
"courseware": {
|
||||||
"courseId": null,
|
"courseId": null,
|
||||||
|
"courseOutline": {},
|
||||||
|
"courseOutlineShouldUpdate": false,
|
||||||
|
"courseOutlineStatus": "loading",
|
||||||
"courseStatus": "loading",
|
"courseStatus": "loading",
|
||||||
|
"coursewareOutlineSidebarSettings": {},
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
"sequenceMightBeUnit": false,
|
"sequenceMightBeUnit": false,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
@@ -22,12 +26,12 @@ Object {
|
|||||||
"learningAssistant": ObjectContaining {
|
"learningAssistant": ObjectContaining {
|
||||||
"conversationId": Any<String>,
|
"conversationId": Any<String>,
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": {
|
||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": {
|
||||||
"canViewCertificate": true,
|
"canViewCertificate": true,
|
||||||
"celebrations": null,
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
"developerMessage": null,
|
"developerMessage": null,
|
||||||
"errorCode": null,
|
"errorCode": null,
|
||||||
@@ -38,39 +42,40 @@ Object {
|
|||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"isEnrolled": false,
|
"isEnrolled": false,
|
||||||
"isMasquerading": false,
|
"isMasquerading": false,
|
||||||
|
"isNewDiscussionSidebarViewEnabled": false,
|
||||||
"isSelfPaced": false,
|
"isSelfPaced": false,
|
||||||
"isStaff": false,
|
"isStaff": false,
|
||||||
"number": "DemoX",
|
"number": "DemoX",
|
||||||
"org": "edX",
|
"org": "edX",
|
||||||
"originalUserIsStaff": false,
|
"originalUserIsStaff": false,
|
||||||
"start": "2013-02-05T05:00:00Z",
|
"start": "2013-02-05T05:00:00Z",
|
||||||
"tabs": Array [
|
"tabs": [
|
||||||
Object {
|
{
|
||||||
"slug": "outline",
|
"slug": "outline",
|
||||||
"title": "Course",
|
"title": "Course",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "discussion",
|
"slug": "discussion",
|
||||||
"title": "Discussion",
|
"title": "Discussion",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "wiki",
|
"slug": "wiki",
|
||||||
"title": "Wiki",
|
"title": "Wiki",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "progress",
|
"slug": "progress",
|
||||||
"title": "Progress",
|
"title": "Progress",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "instructor",
|
"slug": "instructor",
|
||||||
"title": "Instructor",
|
"title": "Instructor",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "dates",
|
"slug": "dates",
|
||||||
"title": "Dates",
|
"title": "Dates",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
@@ -79,7 +84,7 @@ Object {
|
|||||||
"title": "Demonstration Course",
|
"title": "Demonstration Course",
|
||||||
"userTimezone": "UTC",
|
"userTimezone": "UTC",
|
||||||
"username": "MockUser",
|
"username": "MockUser",
|
||||||
"verifiedMode": Object {
|
"verifiedMode": {
|
||||||
"accessExpirationDate": null,
|
"accessExpirationDate": null,
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"currencySymbol": "$",
|
"currencySymbol": "$",
|
||||||
@@ -89,10 +94,10 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"dates": Object {
|
"dates": {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": {
|
||||||
"courseDateBlocks": Array [
|
"courseDateBlocks": [
|
||||||
Object {
|
{
|
||||||
"date": "2020-05-01T17:59:41Z",
|
"date": "2020-05-01T17:59:41Z",
|
||||||
"dateType": "course-start-date",
|
"dateType": "course-start-date",
|
||||||
"description": "",
|
"description": "",
|
||||||
@@ -101,7 +106,7 @@ Object {
|
|||||||
"link": "",
|
"link": "",
|
||||||
"title": "Course Starts",
|
"title": "Course Starts",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"complete": true,
|
"complete": true,
|
||||||
"date": "2020-05-04T02:59:40.942669Z",
|
"date": "2020-05-04T02:59:40.942669Z",
|
||||||
@@ -111,7 +116,7 @@ Object {
|
|||||||
"learnerHasAccess": true,
|
"learnerHasAccess": true,
|
||||||
"title": "Multi Badges Completed",
|
"title": "Multi Badges Completed",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2020-05-05T02:59:40.942669Z",
|
"date": "2020-05-05T02:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -120,7 +125,7 @@ Object {
|
|||||||
"learnerHasAccess": true,
|
"learnerHasAccess": true,
|
||||||
"title": "Multi Badges Past Due",
|
"title": "Multi Badges Past Due",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2020-05-27T02:59:40.942669Z",
|
"date": "2020-05-27T02:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -130,7 +135,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "Both Past Due 1",
|
"title": "Both Past Due 1",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2020-05-27T02:59:40.942669Z",
|
"date": "2020-05-27T02:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -140,7 +145,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "Both Past Due 2",
|
"title": "Both Past Due 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"complete": true,
|
"complete": true,
|
||||||
"date": "2020-05-28T08:59:40.942669Z",
|
"date": "2020-05-28T08:59:40.942669Z",
|
||||||
@@ -151,7 +156,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "One Completed/Due 1",
|
"title": "One Completed/Due 1",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2020-05-28T08:59:40.942669Z",
|
"date": "2020-05-28T08:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -161,7 +166,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "One Completed/Due 2",
|
"title": "One Completed/Due 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"complete": true,
|
"complete": true,
|
||||||
"date": "2020-05-29T08:59:40.942669Z",
|
"date": "2020-05-29T08:59:40.942669Z",
|
||||||
@@ -172,7 +177,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "Both Completed 1",
|
"title": "Both Completed 1",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"complete": true,
|
"complete": true,
|
||||||
"date": "2020-05-29T08:59:40.942669Z",
|
"date": "2020-05-29T08:59:40.942669Z",
|
||||||
@@ -183,7 +188,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "Both Completed 2",
|
"title": "Both Completed 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"date": "2020-06-16T17:59:40.942669Z",
|
"date": "2020-06-16T17:59:40.942669Z",
|
||||||
"dateType": "verified-upgrade-deadline",
|
"dateType": "verified-upgrade-deadline",
|
||||||
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
|
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
|
||||||
@@ -192,7 +197,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "Upgrade to Verified Certificate",
|
"title": "Upgrade to Verified Certificate",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-17T05:59:40.942669Z",
|
"date": "2030-08-17T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -202,7 +207,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "One Verified 1",
|
"title": "One Verified 1",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-17T05:59:40.942669Z",
|
"date": "2030-08-17T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -212,7 +217,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "One Verified 2",
|
"title": "One Verified 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-17T05:59:40.942669Z",
|
"date": "2030-08-17T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -222,7 +227,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "ORA Verified 2",
|
"title": "ORA Verified 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-18T05:59:40.942669Z",
|
"date": "2030-08-18T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -232,7 +237,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "Both Verified 1",
|
"title": "Both Verified 1",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-18T05:59:40.942669Z",
|
"date": "2030-08-18T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -242,7 +247,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "Both Verified 2",
|
"title": "Both Verified 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-19T05:59:40.942669Z",
|
"date": "2030-08-19T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -250,7 +255,7 @@ Object {
|
|||||||
"learnerHasAccess": true,
|
"learnerHasAccess": true,
|
||||||
"title": "One Unreleased 1",
|
"title": "One Unreleased 1",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-19T05:59:40.942669Z",
|
"date": "2030-08-19T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -260,7 +265,7 @@ Object {
|
|||||||
"link": "https://example.com/",
|
"link": "https://example.com/",
|
||||||
"title": "One Unreleased 2",
|
"title": "One Unreleased 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-20T05:59:40.942669Z",
|
"date": "2030-08-20T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -269,7 +274,7 @@ Object {
|
|||||||
"learnerHasAccess": true,
|
"learnerHasAccess": true,
|
||||||
"title": "Both Unreleased 1",
|
"title": "Both Unreleased 1",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"date": "2030-08-20T05:59:40.942669Z",
|
"date": "2030-08-20T05:59:40.942669Z",
|
||||||
"dateType": "assignment-due-date",
|
"dateType": "assignment-due-date",
|
||||||
@@ -278,7 +283,7 @@ Object {
|
|||||||
"learnerHasAccess": true,
|
"learnerHasAccess": true,
|
||||||
"title": "Both Unreleased 2",
|
"title": "Both Unreleased 2",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"date": "2030-08-23T00:00:00Z",
|
"date": "2030-08-23T00:00:00Z",
|
||||||
"dateType": "course-end-date",
|
"dateType": "course-end-date",
|
||||||
"description": "",
|
"description": "",
|
||||||
@@ -287,7 +292,7 @@ Object {
|
|||||||
"link": "",
|
"link": "",
|
||||||
"title": "Course Ends",
|
"title": "Course Ends",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"date": "2030-09-01T00:00:00Z",
|
"date": "2030-09-01T00:00:00Z",
|
||||||
"dateType": "verification-deadline-date",
|
"dateType": "verification-deadline-date",
|
||||||
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
|
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
|
||||||
@@ -297,7 +302,7 @@ Object {
|
|||||||
"title": "Verification Deadline",
|
"title": "Verification Deadline",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"datesBannerInfo": Object {
|
"datesBannerInfo": {
|
||||||
"contentTypeGatingEnabled": false,
|
"contentTypeGatingEnabled": false,
|
||||||
"missedDeadlines": false,
|
"missedDeadlines": false,
|
||||||
"missedGatedContent": false,
|
"missedGatedContent": false,
|
||||||
@@ -309,16 +314,16 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"plugins": Object {},
|
"plugins": {},
|
||||||
"recommendations": Object {
|
"recommendations": {
|
||||||
"recommendationsStatus": "loading",
|
"recommendationsStatus": "loading",
|
||||||
},
|
},
|
||||||
"specialExams": Object {
|
"specialExams": {
|
||||||
"activeAttempt": null,
|
"activeAttempt": null,
|
||||||
"allowProctoringOptOut": false,
|
"allowProctoringOptOut": false,
|
||||||
"apiErrorMsg": "",
|
"apiErrorMsg": "",
|
||||||
"exam": Object {
|
"exam": {
|
||||||
"attempt": Object {
|
"attempt": {
|
||||||
"attempt_code": "",
|
"attempt_code": "",
|
||||||
"attempt_id": null,
|
"attempt_id": null,
|
||||||
"attempt_status": "",
|
"attempt_status": "",
|
||||||
@@ -346,27 +351,27 @@ Object {
|
|||||||
"is_active": true,
|
"is_active": true,
|
||||||
"is_practice_exam": false,
|
"is_practice_exam": false,
|
||||||
"is_proctored": false,
|
"is_proctored": false,
|
||||||
"prerequisite_status": Object {
|
"prerequisite_status": {
|
||||||
"are_prerequisites_satisifed": true,
|
"are_prerequisites_satisifed": true,
|
||||||
"declined_prerequisites": Array [],
|
"declined_prerequisites": [],
|
||||||
"failed_prerequisites": Array [],
|
"failed_prerequisites": [],
|
||||||
"pending_prerequisites": Array [],
|
"pending_prerequisites": [],
|
||||||
"satisfied_prerequisites": Array [],
|
"satisfied_prerequisites": [],
|
||||||
},
|
},
|
||||||
"time_limit_mins": null,
|
"time_limit_mins": null,
|
||||||
"type": "",
|
"type": "",
|
||||||
},
|
},
|
||||||
"examAccessToken": Object {
|
"examAccessToken": {
|
||||||
"exam_access_token": "",
|
"exam_access_token": "",
|
||||||
"exam_access_token_expiration": "",
|
"exam_access_token_expiration": "",
|
||||||
},
|
},
|
||||||
"isLoading": true,
|
"isLoading": true,
|
||||||
"proctoringSettings": Object {
|
"proctoringSettings": {
|
||||||
"exam_proctoring_backend": Object {
|
"exam_proctoring_backend": {
|
||||||
"download_url": "",
|
"download_url": "",
|
||||||
"instructions": Array [],
|
"instructions": [],
|
||||||
"name": "",
|
"name": "",
|
||||||
"rules": Object {},
|
"rules": {},
|
||||||
},
|
},
|
||||||
"integration_specific_email": "",
|
"integration_specific_email": "",
|
||||||
"learner_notification_from_email": "",
|
"learner_notification_from_email": "",
|
||||||
@@ -377,7 +382,7 @@ Object {
|
|||||||
},
|
},
|
||||||
"timeIsOver": false,
|
"timeIsOver": false,
|
||||||
},
|
},
|
||||||
"tours": Object {
|
"tours": {
|
||||||
"showCoursewareTour": false,
|
"showCoursewareTour": false,
|
||||||
"showExistingUserCourseHomeTour": false,
|
"showExistingUserCourseHomeTour": false,
|
||||||
"showNewUserCourseHomeModal": false,
|
"showNewUserCourseHomeModal": false,
|
||||||
@@ -388,8 +393,8 @@ Object {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
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",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
"proctoringPanelStatus": "loading",
|
"proctoringPanelStatus": "loading",
|
||||||
@@ -399,9 +404,13 @@ Object {
|
|||||||
"toastBodyText": null,
|
"toastBodyText": null,
|
||||||
"toastHeader": "",
|
"toastHeader": "",
|
||||||
},
|
},
|
||||||
"courseware": Object {
|
"courseware": {
|
||||||
"courseId": null,
|
"courseId": null,
|
||||||
|
"courseOutline": {},
|
||||||
|
"courseOutlineShouldUpdate": false,
|
||||||
|
"courseOutlineStatus": "loading",
|
||||||
"courseStatus": "loading",
|
"courseStatus": "loading",
|
||||||
|
"coursewareOutlineSidebarSettings": {},
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
"sequenceMightBeUnit": false,
|
"sequenceMightBeUnit": false,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
@@ -409,12 +418,12 @@ Object {
|
|||||||
"learningAssistant": ObjectContaining {
|
"learningAssistant": ObjectContaining {
|
||||||
"conversationId": Any<String>,
|
"conversationId": Any<String>,
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": {
|
||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": {
|
||||||
"canViewCertificate": true,
|
"canViewCertificate": true,
|
||||||
"celebrations": null,
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
"developerMessage": null,
|
"developerMessage": null,
|
||||||
"errorCode": null,
|
"errorCode": null,
|
||||||
@@ -425,39 +434,40 @@ Object {
|
|||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"isEnrolled": false,
|
"isEnrolled": false,
|
||||||
"isMasquerading": false,
|
"isMasquerading": false,
|
||||||
|
"isNewDiscussionSidebarViewEnabled": false,
|
||||||
"isSelfPaced": false,
|
"isSelfPaced": false,
|
||||||
"isStaff": false,
|
"isStaff": false,
|
||||||
"number": "DemoX",
|
"number": "DemoX",
|
||||||
"org": "edX",
|
"org": "edX",
|
||||||
"originalUserIsStaff": false,
|
"originalUserIsStaff": false,
|
||||||
"start": "2013-02-05T05:00:00Z",
|
"start": "2013-02-05T05:00:00Z",
|
||||||
"tabs": Array [
|
"tabs": [
|
||||||
Object {
|
{
|
||||||
"slug": "outline",
|
"slug": "outline",
|
||||||
"title": "Course",
|
"title": "Course",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "discussion",
|
"slug": "discussion",
|
||||||
"title": "Discussion",
|
"title": "Discussion",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "wiki",
|
"slug": "wiki",
|
||||||
"title": "Wiki",
|
"title": "Wiki",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "progress",
|
"slug": "progress",
|
||||||
"title": "Progress",
|
"title": "Progress",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "instructor",
|
"slug": "instructor",
|
||||||
"title": "Instructor",
|
"title": "Instructor",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "dates",
|
"slug": "dates",
|
||||||
"title": "Dates",
|
"title": "Dates",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
@@ -466,7 +476,7 @@ Object {
|
|||||||
"title": "Demonstration Course",
|
"title": "Demonstration Course",
|
||||||
"userTimezone": "UTC",
|
"userTimezone": "UTC",
|
||||||
"username": "MockUser",
|
"username": "MockUser",
|
||||||
"verifiedMode": Object {
|
"verifiedMode": {
|
||||||
"accessExpirationDate": null,
|
"accessExpirationDate": null,
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"currencySymbol": "$",
|
"currencySymbol": "$",
|
||||||
@@ -476,41 +486,41 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"outline": Object {
|
"outline": {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": {
|
||||||
"accessExpiration": null,
|
"accessExpiration": null,
|
||||||
"canShowUpgradeSock": false,
|
"canShowUpgradeSock": false,
|
||||||
"certData": Object {
|
"certData": {
|
||||||
"certStatus": null,
|
"certStatus": null,
|
||||||
"certWebViewUrl": null,
|
"certWebViewUrl": null,
|
||||||
"certificateAvailableDate": null,
|
"certificateAvailableDate": null,
|
||||||
},
|
},
|
||||||
"courseBlocks": Object {
|
"courseBlocks": {
|
||||||
"courses": Object {
|
"courses": {
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
|
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": {
|
||||||
"hasScheduledContent": false,
|
"hasScheduledContent": false,
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"sectionIds": Array [
|
"sectionIds": [
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
],
|
],
|
||||||
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
|
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"sections": Object {
|
"sections": {
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
|
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": {
|
||||||
"complete": false,
|
"complete": false,
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"hideFromTOC": undefined,
|
"hideFromTOC": undefined,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
"resumeBlock": false,
|
"resumeBlock": false,
|
||||||
"sequenceIds": Array [
|
"sequenceIds": [
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||||
],
|
],
|
||||||
"title": "Title of Section",
|
"title": "Title of Section",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"sequences": Object {
|
"sequences": {
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object {
|
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": {
|
||||||
"complete": false,
|
"complete": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"due": null,
|
"due": null,
|
||||||
@@ -526,30 +536,30 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"courseGoals": Object {
|
"courseGoals": {
|
||||||
"daysPerWeek": null,
|
"daysPerWeek": null,
|
||||||
"goalOptions": Array [],
|
"goalOptions": [],
|
||||||
"selectedGoal": null,
|
"selectedGoal": null,
|
||||||
"subscribedToReminders": null,
|
"subscribedToReminders": null,
|
||||||
"weeklyLearningGoalEnabled": false,
|
"weeklyLearningGoalEnabled": false,
|
||||||
},
|
},
|
||||||
"courseTools": Array [
|
"courseTools": [
|
||||||
Object {
|
{
|
||||||
"analyticsId": "edx.bookmarks",
|
"analyticsId": "edx.bookmarks",
|
||||||
"title": "Bookmarks",
|
"title": "Bookmarks",
|
||||||
"url": "https://example.com/bookmarks",
|
"url": "https://example.com/bookmarks",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"datesBannerInfo": Object {
|
"datesBannerInfo": {
|
||||||
"contentTypeGatingEnabled": false,
|
"contentTypeGatingEnabled": false,
|
||||||
"missedDeadlines": false,
|
"missedDeadlines": false,
|
||||||
"missedGatedContent": false,
|
"missedGatedContent": false,
|
||||||
},
|
},
|
||||||
"datesWidget": Object {
|
"datesWidget": {
|
||||||
"courseDateBlocks": Array [],
|
"courseDateBlocks": [],
|
||||||
},
|
},
|
||||||
"enableProctoredExams": undefined,
|
"enableProctoredExams": undefined,
|
||||||
"enrollAlert": Object {
|
"enrollAlert": {
|
||||||
"canEnroll": true,
|
"canEnroll": true,
|
||||||
"extraText": "Contact the administrator.",
|
"extraText": "Contact the administrator.",
|
||||||
},
|
},
|
||||||
@@ -559,13 +569,13 @@ Object {
|
|||||||
"hasScheduledContent": null,
|
"hasScheduledContent": null,
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"offer": null,
|
"offer": null,
|
||||||
"resumeCourse": Object {
|
"resumeCourse": {
|
||||||
"hasVisitedCourse": false,
|
"hasVisitedCourse": false,
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||||
},
|
},
|
||||||
"timeOffsetMillis": 0,
|
"timeOffsetMillis": 0,
|
||||||
"userHasPassingGrade": undefined,
|
"userHasPassingGrade": undefined,
|
||||||
"verifiedMode": Object {
|
"verifiedMode": {
|
||||||
"accessExpirationDate": "2050-01-01T12:00:00",
|
"accessExpirationDate": "2050-01-01T12:00:00",
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"currencySymbol": "$",
|
"currencySymbol": "$",
|
||||||
@@ -577,16 +587,16 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"plugins": Object {},
|
"plugins": {},
|
||||||
"recommendations": Object {
|
"recommendations": {
|
||||||
"recommendationsStatus": "loading",
|
"recommendationsStatus": "loading",
|
||||||
},
|
},
|
||||||
"specialExams": Object {
|
"specialExams": {
|
||||||
"activeAttempt": null,
|
"activeAttempt": null,
|
||||||
"allowProctoringOptOut": false,
|
"allowProctoringOptOut": false,
|
||||||
"apiErrorMsg": "",
|
"apiErrorMsg": "",
|
||||||
"exam": Object {
|
"exam": {
|
||||||
"attempt": Object {
|
"attempt": {
|
||||||
"attempt_code": "",
|
"attempt_code": "",
|
||||||
"attempt_id": null,
|
"attempt_id": null,
|
||||||
"attempt_status": "",
|
"attempt_status": "",
|
||||||
@@ -614,27 +624,27 @@ Object {
|
|||||||
"is_active": true,
|
"is_active": true,
|
||||||
"is_practice_exam": false,
|
"is_practice_exam": false,
|
||||||
"is_proctored": false,
|
"is_proctored": false,
|
||||||
"prerequisite_status": Object {
|
"prerequisite_status": {
|
||||||
"are_prerequisites_satisifed": true,
|
"are_prerequisites_satisifed": true,
|
||||||
"declined_prerequisites": Array [],
|
"declined_prerequisites": [],
|
||||||
"failed_prerequisites": Array [],
|
"failed_prerequisites": [],
|
||||||
"pending_prerequisites": Array [],
|
"pending_prerequisites": [],
|
||||||
"satisfied_prerequisites": Array [],
|
"satisfied_prerequisites": [],
|
||||||
},
|
},
|
||||||
"time_limit_mins": null,
|
"time_limit_mins": null,
|
||||||
"type": "",
|
"type": "",
|
||||||
},
|
},
|
||||||
"examAccessToken": Object {
|
"examAccessToken": {
|
||||||
"exam_access_token": "",
|
"exam_access_token": "",
|
||||||
"exam_access_token_expiration": "",
|
"exam_access_token_expiration": "",
|
||||||
},
|
},
|
||||||
"isLoading": true,
|
"isLoading": true,
|
||||||
"proctoringSettings": Object {
|
"proctoringSettings": {
|
||||||
"exam_proctoring_backend": Object {
|
"exam_proctoring_backend": {
|
||||||
"download_url": "",
|
"download_url": "",
|
||||||
"instructions": Array [],
|
"instructions": [],
|
||||||
"name": "",
|
"name": "",
|
||||||
"rules": Object {},
|
"rules": {},
|
||||||
},
|
},
|
||||||
"integration_specific_email": "",
|
"integration_specific_email": "",
|
||||||
"learner_notification_from_email": "",
|
"learner_notification_from_email": "",
|
||||||
@@ -645,7 +655,7 @@ Object {
|
|||||||
},
|
},
|
||||||
"timeIsOver": false,
|
"timeIsOver": false,
|
||||||
},
|
},
|
||||||
"tours": Object {
|
"tours": {
|
||||||
"showCoursewareTour": false,
|
"showCoursewareTour": false,
|
||||||
"showExistingUserCourseHomeTour": false,
|
"showExistingUserCourseHomeTour": false,
|
||||||
"showNewUserCourseHomeModal": false,
|
"showNewUserCourseHomeModal": false,
|
||||||
@@ -656,8 +666,8 @@ Object {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
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",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
"proctoringPanelStatus": "loading",
|
"proctoringPanelStatus": "loading",
|
||||||
@@ -667,9 +677,13 @@ Object {
|
|||||||
"toastBodyText": null,
|
"toastBodyText": null,
|
||||||
"toastHeader": "",
|
"toastHeader": "",
|
||||||
},
|
},
|
||||||
"courseware": Object {
|
"courseware": {
|
||||||
"courseId": null,
|
"courseId": null,
|
||||||
|
"courseOutline": {},
|
||||||
|
"courseOutlineShouldUpdate": false,
|
||||||
|
"courseOutlineStatus": "loading",
|
||||||
"courseStatus": "loading",
|
"courseStatus": "loading",
|
||||||
|
"coursewareOutlineSidebarSettings": {},
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
"sequenceMightBeUnit": false,
|
"sequenceMightBeUnit": false,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
@@ -677,12 +691,12 @@ Object {
|
|||||||
"learningAssistant": ObjectContaining {
|
"learningAssistant": ObjectContaining {
|
||||||
"conversationId": Any<String>,
|
"conversationId": Any<String>,
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": {
|
||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": {
|
||||||
"canViewCertificate": true,
|
"canViewCertificate": true,
|
||||||
"celebrations": null,
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
"developerMessage": null,
|
"developerMessage": null,
|
||||||
"errorCode": null,
|
"errorCode": null,
|
||||||
@@ -693,39 +707,40 @@ Object {
|
|||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"isEnrolled": false,
|
"isEnrolled": false,
|
||||||
"isMasquerading": false,
|
"isMasquerading": false,
|
||||||
|
"isNewDiscussionSidebarViewEnabled": false,
|
||||||
"isSelfPaced": false,
|
"isSelfPaced": false,
|
||||||
"isStaff": false,
|
"isStaff": false,
|
||||||
"number": "DemoX",
|
"number": "DemoX",
|
||||||
"org": "edX",
|
"org": "edX",
|
||||||
"originalUserIsStaff": false,
|
"originalUserIsStaff": false,
|
||||||
"start": "2013-02-05T05:00:00Z",
|
"start": "2013-02-05T05:00:00Z",
|
||||||
"tabs": Array [
|
"tabs": [
|
||||||
Object {
|
{
|
||||||
"slug": "outline",
|
"slug": "outline",
|
||||||
"title": "Course",
|
"title": "Course",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "discussion",
|
"slug": "discussion",
|
||||||
"title": "Discussion",
|
"title": "Discussion",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "wiki",
|
"slug": "wiki",
|
||||||
"title": "Wiki",
|
"title": "Wiki",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "progress",
|
"slug": "progress",
|
||||||
"title": "Progress",
|
"title": "Progress",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "instructor",
|
"slug": "instructor",
|
||||||
"title": "Instructor",
|
"title": "Instructor",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"slug": "dates",
|
"slug": "dates",
|
||||||
"title": "Dates",
|
"title": "Dates",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
@@ -734,7 +749,7 @@ Object {
|
|||||||
"title": "Demonstration Course",
|
"title": "Demonstration Course",
|
||||||
"userTimezone": "UTC",
|
"userTimezone": "UTC",
|
||||||
"username": "MockUser",
|
"username": "MockUser",
|
||||||
"verifiedMode": Object {
|
"verifiedMode": {
|
||||||
"accessExpirationDate": null,
|
"accessExpirationDate": null,
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"currencySymbol": "$",
|
"currencySymbol": "$",
|
||||||
@@ -744,16 +759,16 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"progress": Object {
|
"progress": {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": {
|
||||||
"accessExpiration": null,
|
"accessExpiration": null,
|
||||||
"certificateData": Object {},
|
"certificateData": {},
|
||||||
"completionSummary": Object {
|
"completionSummary": {
|
||||||
"completeCount": 1,
|
"completeCount": 1,
|
||||||
"incompleteCount": 1,
|
"incompleteCount": 1,
|
||||||
"lockedCount": 0,
|
"lockedCount": 0,
|
||||||
},
|
},
|
||||||
"courseGrade": Object {
|
"courseGrade": {
|
||||||
"isPassing": true,
|
"isPassing": true,
|
||||||
"letterGrade": "pass",
|
"letterGrade": "pass",
|
||||||
"percent": 1,
|
"percent": 1,
|
||||||
@@ -764,10 +779,10 @@ Object {
|
|||||||
"enrollmentMode": "audit",
|
"enrollmentMode": "audit",
|
||||||
"gradesFeatureIsFullyLocked": false,
|
"gradesFeatureIsFullyLocked": false,
|
||||||
"gradesFeatureIsPartiallyLocked": false,
|
"gradesFeatureIsPartiallyLocked": false,
|
||||||
"gradingPolicy": Object {
|
"gradingPolicy": {
|
||||||
"assignmentPolicies": Array [
|
"assignmentPolicies": [
|
||||||
Object {
|
{
|
||||||
"averageGrade": "1.00",
|
"averageGrade": "1.0000",
|
||||||
"numDroppable": 1,
|
"numDroppable": 1,
|
||||||
"shortLabel": "HW",
|
"shortLabel": "HW",
|
||||||
"type": "Homework",
|
"type": "Homework",
|
||||||
@@ -775,17 +790,17 @@ Object {
|
|||||||
"weightedGrade": 1,
|
"weightedGrade": 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"gradeRange": Object {
|
"gradeRange": {
|
||||||
"pass": 0.75,
|
"pass": 0.75,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"hasScheduledContent": false,
|
"hasScheduledContent": false,
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"sectionScores": Array [
|
"sectionScores": [
|
||||||
Object {
|
{
|
||||||
"displayName": "First section",
|
"displayName": "First section",
|
||||||
"subsections": Array [
|
"subsections": [
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
|
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
|
||||||
"displayName": "First subsection",
|
"displayName": "First subsection",
|
||||||
@@ -794,16 +809,16 @@ Object {
|
|||||||
"numPointsEarned": 0,
|
"numPointsEarned": 0,
|
||||||
"numPointsPossible": 3,
|
"numPointsPossible": 3,
|
||||||
"percentGraded": 0,
|
"percentGraded": 0,
|
||||||
"problemScores": Array [
|
"problemScores": [
|
||||||
Object {
|
{
|
||||||
"earned": 0,
|
"earned": 0,
|
||||||
"possible": 1,
|
"possible": 1,
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"earned": 0,
|
"earned": 0,
|
||||||
"possible": 1,
|
"possible": 1,
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"earned": 0,
|
"earned": 0,
|
||||||
"possible": 1,
|
"possible": 1,
|
||||||
},
|
},
|
||||||
@@ -814,18 +829,18 @@ Object {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Object {
|
{
|
||||||
"displayName": "Second section",
|
"displayName": "Second section",
|
||||||
"subsections": Array [
|
"subsections": [
|
||||||
Object {
|
{
|
||||||
"assignmentType": "Homework",
|
"assignmentType": "Homework",
|
||||||
"displayName": "Second subsection",
|
"displayName": "Second subsection",
|
||||||
"hasGradedAssignment": true,
|
"hasGradedAssignment": true,
|
||||||
"numPointsEarned": 1,
|
"numPointsEarned": 1,
|
||||||
"numPointsPossible": 1,
|
"numPointsPossible": 1,
|
||||||
"percentGraded": 1,
|
"percentGraded": 1,
|
||||||
"problemScores": Array [
|
"problemScores": [
|
||||||
Object {
|
{
|
||||||
"earned": 1,
|
"earned": 1,
|
||||||
"possible": 1,
|
"possible": 1,
|
||||||
},
|
},
|
||||||
@@ -839,7 +854,7 @@ Object {
|
|||||||
],
|
],
|
||||||
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
|
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
|
||||||
"userHasPassingGrade": false,
|
"userHasPassingGrade": false,
|
||||||
"verificationData": Object {
|
"verificationData": {
|
||||||
"link": null,
|
"link": null,
|
||||||
"status": "none",
|
"status": "none",
|
||||||
"statusDate": null,
|
"statusDate": null,
|
||||||
@@ -848,16 +863,16 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"plugins": Object {},
|
"plugins": {},
|
||||||
"recommendations": Object {
|
"recommendations": {
|
||||||
"recommendationsStatus": "loading",
|
"recommendationsStatus": "loading",
|
||||||
},
|
},
|
||||||
"specialExams": Object {
|
"specialExams": {
|
||||||
"activeAttempt": null,
|
"activeAttempt": null,
|
||||||
"allowProctoringOptOut": false,
|
"allowProctoringOptOut": false,
|
||||||
"apiErrorMsg": "",
|
"apiErrorMsg": "",
|
||||||
"exam": Object {
|
"exam": {
|
||||||
"attempt": Object {
|
"attempt": {
|
||||||
"attempt_code": "",
|
"attempt_code": "",
|
||||||
"attempt_id": null,
|
"attempt_id": null,
|
||||||
"attempt_status": "",
|
"attempt_status": "",
|
||||||
@@ -885,27 +900,27 @@ Object {
|
|||||||
"is_active": true,
|
"is_active": true,
|
||||||
"is_practice_exam": false,
|
"is_practice_exam": false,
|
||||||
"is_proctored": false,
|
"is_proctored": false,
|
||||||
"prerequisite_status": Object {
|
"prerequisite_status": {
|
||||||
"are_prerequisites_satisifed": true,
|
"are_prerequisites_satisifed": true,
|
||||||
"declined_prerequisites": Array [],
|
"declined_prerequisites": [],
|
||||||
"failed_prerequisites": Array [],
|
"failed_prerequisites": [],
|
||||||
"pending_prerequisites": Array [],
|
"pending_prerequisites": [],
|
||||||
"satisfied_prerequisites": Array [],
|
"satisfied_prerequisites": [],
|
||||||
},
|
},
|
||||||
"time_limit_mins": null,
|
"time_limit_mins": null,
|
||||||
"type": "",
|
"type": "",
|
||||||
},
|
},
|
||||||
"examAccessToken": Object {
|
"examAccessToken": {
|
||||||
"exam_access_token": "",
|
"exam_access_token": "",
|
||||||
"exam_access_token_expiration": "",
|
"exam_access_token_expiration": "",
|
||||||
},
|
},
|
||||||
"isLoading": true,
|
"isLoading": true,
|
||||||
"proctoringSettings": Object {
|
"proctoringSettings": {
|
||||||
"exam_proctoring_backend": Object {
|
"exam_proctoring_backend": {
|
||||||
"download_url": "",
|
"download_url": "",
|
||||||
"instructions": Array [],
|
"instructions": [],
|
||||||
"name": "",
|
"name": "",
|
||||||
"rules": Object {},
|
"rules": {},
|
||||||
},
|
},
|
||||||
"integration_specific_email": "",
|
"integration_specific_email": "",
|
||||||
"learner_notification_from_email": "",
|
"learner_notification_from_email": "",
|
||||||
@@ -916,7 +931,7 @@ Object {
|
|||||||
},
|
},
|
||||||
"timeIsOver": false,
|
"timeIsOver": false,
|
||||||
},
|
},
|
||||||
"tours": Object {
|
"tours": {
|
||||||
"showCoursewareTour": false,
|
"showCoursewareTour": false,
|
||||||
"showExistingUserCourseHomeTour": false,
|
"showExistingUserCourseHomeTour": false,
|
||||||
"showNewUserCourseHomeModal": 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
|
// 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
|
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
|
||||||
// exists in edx-platform.
|
// 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;
|
weightedGrade = averageGrade * assignmentWeight;
|
||||||
}
|
}
|
||||||
return { averageGrade, weightedGrade };
|
return { averageGrade, weightedGrade };
|
||||||
@@ -289,9 +289,17 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getProctoringInfoData(courseId, username) {
|
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)}`;
|
let url;
|
||||||
if (username) {
|
if (!getConfig().EXAMS_BASE_URL) {
|
||||||
url += `&username=${encodeURIComponent(username)}`;
|
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 {
|
try {
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
@@ -449,7 +457,7 @@ export async function unsubscribeFromCourseGoal(token) {
|
|||||||
.then(res => camelCaseObject(res));
|
.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 url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`);
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url.href);
|
const { data } = await getAuthenticatedHttpClient().get(url.href);
|
||||||
return { enabled: data.enabled || false };
|
return { enabled: data.enabled || false };
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ describe('Course Home Service', () => {
|
|||||||
}),
|
}),
|
||||||
title: string('Demonstration Course'),
|
title: string('Demonstration Course'),
|
||||||
username: string('edx'),
|
username: string('edx'),
|
||||||
|
has_course_author_access: boolean(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -133,6 +134,7 @@ describe('Course Home Service', () => {
|
|||||||
],
|
],
|
||||||
title: 'Demonstration Course',
|
title: 'Demonstration Course',
|
||||||
username: 'edx',
|
username: 'edx',
|
||||||
|
hasCourseAuthorAccess: true,
|
||||||
};
|
};
|
||||||
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export const LOADING = 'loading';
|
import {
|
||||||
export const LOADED = 'loaded';
|
LOADING,
|
||||||
export const FAILED = 'failed';
|
LOADED,
|
||||||
export const DENIED = 'denied';
|
FAILED,
|
||||||
|
DENIED,
|
||||||
|
} from '@src/constants';
|
||||||
|
|
||||||
const slice = createSlice({
|
const slice = createSlice({
|
||||||
name: 'course-home',
|
name: 'course-home',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
postDismissWelcomeMessage,
|
postDismissWelcomeMessage,
|
||||||
postRequestCert,
|
postRequestCert,
|
||||||
getLiveTabIframe,
|
getLiveTabIframe,
|
||||||
getCoursewareSearchEnabledFlag,
|
getCoursewareSearchEnabled,
|
||||||
searchCourseContentFromAPI,
|
searchCourseContentFromAPI,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export function processEvent(eventData, getTabData) {
|
|||||||
|
|
||||||
export async function fetchCoursewareSearchSettings(courseId) {
|
export async function fetchCoursewareSearchSettings(courseId) {
|
||||||
try {
|
try {
|
||||||
const { enabled } = await getCoursewareSearchEnabledFlag(courseId);
|
const { enabled } = await getCoursewareSearchEnabled(courseId);
|
||||||
return { enabled };
|
return { enabled };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { enabled: false };
|
return { enabled: false };
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ describe('DatesTab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows extra info', async () => {
|
it('shows extra info', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
const { items } = await getDay('Sat, Aug 17, 2030');
|
const { items } = await getDay('Sat, Aug 17, 2030');
|
||||||
expect(items).toHaveLength(3);
|
expect(items).toHaveLength(3);
|
||||||
|
|
||||||
@@ -142,10 +143,12 @@ describe('DatesTab', () => {
|
|||||||
const tipText = "ORA Dates are set by the instructor, and can't be changed";
|
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
|
expect(screen.queryByText(tipText)).toBeNull(); // tooltip does not start in DOM
|
||||||
userEvent.hover(tipIcon);
|
await user.hover(tipIcon);
|
||||||
const tooltip = screen.getByText(tipText); // now it's there
|
screen.getByText(tipText); // now it's there
|
||||||
userEvent.unhover(tipIcon);
|
await user.unhover(tipIcon);
|
||||||
await waitForElementToBeRemoved(tooltip); // and it's gone again
|
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 { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
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 PageLoading from '../../generic/PageLoading';
|
||||||
import { unsubscribeFromCourseGoal } from '../data/api';
|
import { unsubscribeFromCourseGoal } from '../data/api';
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const GoalUnsubscribe = ({ intl }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header showUserDropdown={false} />
|
<HeaderSlot showUserDropdown={false} />
|
||||||
<main id="main-content" className="container my-5 text-center">
|
<main id="main-content" className="container my-5 text-center">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<PageLoading srMessage={`${intl.formatMessage(messages.loading)}`} />
|
<PageLoading srMessage={`${intl.formatMessage(messages.loading)}`} />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
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 { Button } from '@openedx/paragon';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
import { AlertList } from '../../generic/user-messages';
|
import { AlertList } from '../../generic/user-messages';
|
||||||
|
|
||||||
import CourseDates from './widgets/CourseDates';
|
import CourseDates from './widgets/CourseDates';
|
||||||
@@ -14,7 +15,6 @@ import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
|
|||||||
import CourseTools from './widgets/CourseTools';
|
import CourseTools from './widgets/CourseTools';
|
||||||
import { fetchOutlineTab } from '../data';
|
import { fetchOutlineTab } from '../data';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import Section from './Section';
|
|
||||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
||||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||||
@@ -27,8 +27,10 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import WelcomeMessage from './widgets/WelcomeMessage';
|
import WelcomeMessage from './widgets/WelcomeMessage';
|
||||||
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
||||||
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
|
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
|
||||||
|
import CourseHomeSectionOutlineSlot from '../../plugin-slots/CourseHomeSectionOutlineSlot';
|
||||||
|
|
||||||
const OutlineTab = ({ intl }) => {
|
const OutlineTab = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
proctoringPanelStatus,
|
proctoringPanelStatus,
|
||||||
@@ -41,6 +43,8 @@ const OutlineTab = ({ intl }) => {
|
|||||||
userTimezone,
|
userTimezone,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
|
const expandButtonRef = useRef();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
courseBlocks: {
|
courseBlocks: {
|
||||||
@@ -158,27 +162,21 @@ const OutlineTab = ({ intl }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<StartOrResumeCourseCard />
|
<StartOrResumeCourseCard />
|
||||||
<WelcomeMessage courseId={courseId} />
|
<WelcomeMessage courseId={courseId} nextElementRef={expandButtonRef} />
|
||||||
{rootCourseId && (
|
{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">
|
<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)}
|
{expandAll ? intl.formatMessage(messages.collapseAll) : intl.formatMessage(messages.expandAll)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ol id="courseHome-outline" className="list-unstyled">
|
<CourseHomeSectionOutlineSlot
|
||||||
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
expandAll={expandAll}
|
||||||
<Section
|
sectionIds={courses[rootCourseId].sectionIds}
|
||||||
key={sectionId}
|
sections={sections}
|
||||||
courseId={courseId}
|
/>
|
||||||
defaultOpen={sections[sectionId].resumeBlock}
|
|
||||||
expand={expandAll}
|
|
||||||
section={sections[sectionId]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -194,19 +192,27 @@ const OutlineTab = ({ intl }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CourseTools />
|
<CourseTools />
|
||||||
<UpgradeNotification
|
<PluginSlot
|
||||||
offer={offer}
|
id="outline_tab_notifications_slot"
|
||||||
verifiedMode={verifiedMode}
|
pluginProps={{
|
||||||
accessExpiration={accessExpiration}
|
courseId,
|
||||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
model: 'outline',
|
||||||
marketingUrl={marketingUrl}
|
}}
|
||||||
upsellPageName="course_home"
|
>
|
||||||
userTimezone={userTimezone}
|
<UpgradeNotification
|
||||||
shouldDisplayBorder
|
offer={offer}
|
||||||
timeOffsetMillis={timeOffsetMillis}
|
verifiedMode={verifiedMode}
|
||||||
courseId={courseId}
|
accessExpiration={accessExpiration}
|
||||||
org={org}
|
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||||
/>
|
marketingUrl={marketingUrl}
|
||||||
|
upsellPageName="course_home"
|
||||||
|
userTimezone={userTimezone}
|
||||||
|
shouldDisplayBorder
|
||||||
|
timeOffsetMillis={timeOffsetMillis}
|
||||||
|
courseId={courseId}
|
||||||
|
org={org}
|
||||||
|
/>
|
||||||
|
</PluginSlot>
|
||||||
<CourseDates />
|
<CourseDates />
|
||||||
<CourseHandouts />
|
<CourseHandouts />
|
||||||
</div>
|
</div>
|
||||||
@@ -216,8 +222,4 @@ const OutlineTab = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
OutlineTab.propTypes = {
|
export default OutlineTab;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(OutlineTab);
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe('Outline Tab', () => {
|
|||||||
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
||||||
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
||||||
const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
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 store = initializeStore();
|
||||||
const defaultMetadata = Factory.build('courseHomeMetadata');
|
const defaultMetadata = Factory.build('courseHomeMetadata');
|
||||||
@@ -132,7 +132,18 @@ describe('Outline Tab', () => {
|
|||||||
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
|
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes outline_tab_notifications_slot', async () => {
|
||||||
|
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
||||||
|
setTabData({
|
||||||
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('outline_tab_notifications_slot')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('handles expand/collapse all button click', async () => {
|
it('handles expand/collapse all button click', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
// Button renders as "Expand All"
|
// Button renders as "Expand All"
|
||||||
const expandButton = screen.getByRole('button', { name: 'Expand all' });
|
const expandButton = screen.getByRole('button', { name: 'Expand all' });
|
||||||
@@ -143,11 +154,11 @@ describe('Outline Tab', () => {
|
|||||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
// Click to expand section
|
// Click to expand section
|
||||||
userEvent.click(expandButton);
|
await user.click(expandButton);
|
||||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
||||||
|
|
||||||
// Click to collapse section
|
// Click to collapse section
|
||||||
userEvent.click(expandButton);
|
await user.click(expandButton);
|
||||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +168,7 @@ describe('Outline Tab', () => {
|
|||||||
course_blocks: { blocks: courseBlocks.blocks },
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByTitle('Completed section')).toBeInTheDocument();
|
expect(screen.getByLabelText('Completed section')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays correct icon for incomplete assignment', async () => {
|
it('displays correct icon for incomplete assignment', async () => {
|
||||||
@@ -166,7 +177,7 @@ describe('Outline Tab', () => {
|
|||||||
course_blocks: { blocks: courseBlocks.blocks },
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
|
expect(screen.getByLabelText('Incomplete section')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SequenceLink displays link', async () => {
|
it('SequenceLink displays link', async () => {
|
||||||
@@ -265,21 +276,34 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders show more/less button and handles click', async () => {
|
it('renders show more/less button and handles click', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
||||||
let showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
let showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
||||||
expect(showMoreButton).toBeInTheDocument();
|
expect(showMoreButton).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.click(showMoreButton);
|
await user.click(showMoreButton);
|
||||||
let showLessButton = screen.getByRole('button', { name: 'Show Less' });
|
let showLessButton = screen.getByRole('button', { name: 'Show Less' });
|
||||||
expect(showLessButton).toBeInTheDocument();
|
expect(showLessButton).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('long-welcome-message-iframe')).toBeInTheDocument();
|
expect(screen.getByTestId('long-welcome-message-iframe')).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.click(showLessButton);
|
await user.click(showLessButton);
|
||||||
showLessButton = screen.queryByRole('button', { name: 'Show Less' });
|
showLessButton = screen.queryByRole('button', { name: 'Show Less' });
|
||||||
expect(showLessButton).not.toBeInTheDocument();
|
expect(showLessButton).not.toBeInTheDocument();
|
||||||
showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
||||||
expect(showMoreButton).toBeInTheDocument();
|
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 () => {
|
it('ignores comments and misformatted HTML', async () => {
|
||||||
|
|||||||
@@ -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,148 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
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="true"
|
|
||||||
title={intl.formatMessage(messages.completedAssignment)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={farCheckCircle}
|
|
||||||
fixedWidth
|
|
||||||
className="float-left text-gray-400 mt-1"
|
|
||||||
aria-hidden="true"
|
|
||||||
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',
|
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',
|
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;
|
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 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 { Alert, Button, TransitionReplace } from '@openedx/paragon';
|
||||||
import truncate from 'truncate-html';
|
import truncate from 'truncate-html';
|
||||||
|
|
||||||
@@ -11,11 +11,13 @@ import messages from '../messages';
|
|||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import { dismissWelcomeMessage } from '../../data/thunks';
|
import { dismissWelcomeMessage } from '../../data/thunks';
|
||||||
|
|
||||||
const WelcomeMessage = ({ courseId, intl }) => {
|
const WelcomeMessage = ({ courseId, nextElementRef }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
welcomeMessageHtml,
|
welcomeMessageHtml,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
const messageBodyRef = useRef();
|
||||||
const [display, setDisplay] = useState(true);
|
const [display, setDisplay] = useState(true);
|
||||||
|
|
||||||
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
|
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
|
||||||
@@ -49,13 +51,20 @@ const WelcomeMessage = ({ courseId, intl }) => {
|
|||||||
dismissible
|
dismissible
|
||||||
show={display}
|
show={display}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
nextElementRef.current?.focus();
|
||||||
setDisplay(false);
|
setDisplay(false);
|
||||||
dispatch(dismissWelcomeMessage(courseId));
|
dispatch(dismissWelcomeMessage(courseId));
|
||||||
}}
|
}}
|
||||||
className="raised-card"
|
className="raised-card"
|
||||||
actions={messageCanBeShortened ? [
|
actions={messageCanBeShortened ? [
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowShortMessage(!showShortMessage)}
|
onClick={() => {
|
||||||
|
if (showShortMessage) {
|
||||||
|
messageBodyRef.current?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowShortMessage(!showShortMessage);
|
||||||
|
}}
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
>
|
>
|
||||||
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
|
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
|
||||||
@@ -63,32 +72,34 @@ const WelcomeMessage = ({ courseId, intl }) => {
|
|||||||
</Button>,
|
</Button>,
|
||||||
] : []}
|
] : []}
|
||||||
>
|
>
|
||||||
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
|
<div ref={messageBodyRef} tabIndex="-1">
|
||||||
{showShortMessage ? (
|
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
|
||||||
<LmsHtmlFragment
|
{showShortMessage ? (
|
||||||
className="inline-link"
|
<LmsHtmlFragment
|
||||||
data-testid="short-welcome-message-iframe"
|
className="inline-link"
|
||||||
key="short-html"
|
data-testid="short-welcome-message-iframe"
|
||||||
html={shortWelcomeMessageHtml}
|
key="short-html"
|
||||||
title={intl.formatMessage(messages.welcomeMessage)}
|
html={shortWelcomeMessageHtml}
|
||||||
/>
|
title={intl.formatMessage(messages.welcomeMessage)}
|
||||||
) : (
|
/>
|
||||||
<LmsHtmlFragment
|
) : (
|
||||||
className="inline-link"
|
<LmsHtmlFragment
|
||||||
data-testid="long-welcome-message-iframe"
|
className="inline-link"
|
||||||
key="full-html"
|
data-testid="long-welcome-message-iframe"
|
||||||
html={cleanedWelcomeMessageHtml}
|
key="full-html"
|
||||||
title={intl.formatMessage(messages.welcomeMessage)}
|
html={cleanedWelcomeMessageHtml}
|
||||||
/>
|
title={intl.formatMessage(messages.welcomeMessage)}
|
||||||
)}
|
/>
|
||||||
</TransitionReplace>
|
)}
|
||||||
|
</TransitionReplace>
|
||||||
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
WelcomeMessage.propTypes = {
|
WelcomeMessage.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
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 { 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 { Button } from '@openedx/paragon';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const ProgressHeader = ({ intl }) => {
|
const ProgressHeader = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
targetUserId,
|
targetUserId,
|
||||||
@@ -37,8 +36,4 @@ const ProgressHeader = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ProgressHeader.propTypes = {
|
export default ProgressHeader;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(ProgressHeader);
|
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useWindowSize } from '@openedx/paragon';
|
||||||
import { breakpoints, 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 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 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';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
const ProgressTab = () => {
|
const ProgressTab = () => {
|
||||||
const {
|
const courseId = useContextId();
|
||||||
courseId,
|
const { disableProgressGraph } = useModel('progress', courseId);
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
|
||||||
gradesFeatureIsFullyLocked, disableProgressGraph,
|
|
||||||
} = useModel('progress', courseId);
|
|
||||||
|
|
||||||
const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
|
||||||
|
|
||||||
const windowWidth = useWindowSize().width;
|
const windowWidth = useWindowSize().width;
|
||||||
if (windowWidth === undefined) {
|
if (windowWidth === undefined) {
|
||||||
@@ -31,7 +24,6 @@ const ProgressTab = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wideScreen = windowWidth >= breakpoints.large.minWidth;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProgressHeader />
|
<ProgressHeader />
|
||||||
@@ -39,18 +31,15 @@ const ProgressTab = () => {
|
|||||||
{/* Main body */}
|
{/* Main body */}
|
||||||
<div className="col-12 col-md-8 p-0">
|
<div className="col-12 col-md-8 p-0">
|
||||||
{!disableProgressGraph && <CourseCompletion />}
|
{!disableProgressGraph && <CourseCompletion />}
|
||||||
{!wideScreen && <CertificateStatus />}
|
<ProgressTabCertificateStatusMainBodySlot />
|
||||||
<CourseGrade />
|
<ProgressTabCourseGradeSlot />
|
||||||
<div className={`grades my-4 p-4 rounded raised-card ${applyLockedOverlay}`} aria-hidden={gradesFeatureIsFullyLocked}>
|
<ProgressTabGradeBreakdownSlot />
|
||||||
<GradeSummary />
|
|
||||||
<DetailedGrades />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Side panel */}
|
{/* Side panel */}
|
||||||
<div className="col-12 col-md-4 p-0 px-md-4">
|
<div className="col-12 col-md-4 p-0 px-md-4">
|
||||||
{wideScreen && <CertificateStatus />}
|
<ProgressTabCertificateStatusSidePanelSlot />
|
||||||
<RelatedLinks />
|
<ProgressTabRelatedLinksSlot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Factory } from 'rosie';
|
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 { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { breakpoints } from '@openedx/paragon';
|
import { breakpoints } from '@openedx/paragon';
|
||||||
@@ -111,7 +111,7 @@ describe('Progress Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
sendTrackEvent.mockClear();
|
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
|
fireEvent.click(outlineTabLink[1]); // outlineTabLink[0] corresponds to the link in the DetailedGrades component
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
@@ -471,9 +471,12 @@ describe('Progress Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByText('limited feature')).toBeInTheDocument();
|
expect(screen.getByText('limited feature')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Unlock to work towards a certificate.')).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 () => {
|
it('does not render subsections for which showGrades is false', async () => {
|
||||||
@@ -545,6 +548,111 @@ describe('Progress Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
|
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', () => {
|
describe('Grade Summary', () => {
|
||||||
@@ -788,7 +896,7 @@ describe('Progress Tab', () => {
|
|||||||
sendTrackEvent.mockClear();
|
sendTrackEvent.mockClear();
|
||||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
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);
|
fireEvent.click(outlineLink);
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
@@ -809,7 +917,7 @@ describe('Progress Tab', () => {
|
|||||||
|
|
||||||
// Open the problem score drawer
|
// Open the problem score drawer
|
||||||
fireEvent.click(problemScoreDrawerToggle);
|
fireEvent.click(problemScoreDrawerToggle);
|
||||||
expect(screen.getByText('Problem Scores:')).toBeInTheDocument();
|
expect(screen.getAllByText('Graded Scores:').length).toBeGreaterThan(1);
|
||||||
expect(screen.getAllByText('0/1')).toHaveLength(3);
|
expect(screen.getAllByText('0/1')).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -821,6 +929,14 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||||
expect(screen.getByText('You currently have no graded problem scores.')).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', () => {
|
describe('Certificate Status', () => {
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import React, { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import {
|
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import { Button, Card } from '@openedx/paragon';
|
import { Button, Card } from '@openedx/paragon';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useContextId } from '../../../data/hooks';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
|
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
|
||||||
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
|
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
|
||||||
import { requestCert } from '../../data/thunks';
|
import { requestCert } from '../../data/thunks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import ProgressCertificateStatusSlot from '../../../plugin-slots/ProgressCertificateStatusSlot';
|
||||||
|
|
||||||
|
const CertificateStatus = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const courseId = useContextId();
|
||||||
|
|
||||||
const CertificateStatus = ({ intl }) => {
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
entranceExamData,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useModel('coursewareMeta', courseId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEnrolled,
|
isEnrolled,
|
||||||
@@ -42,6 +45,8 @@ const CertificateStatus = ({ intl }) => {
|
|||||||
certificateAvailableDate,
|
certificateAvailableDate,
|
||||||
} = certificateData || {};
|
} = certificateData || {};
|
||||||
|
|
||||||
|
const entranceExamPassed = entranceExamData?.entranceExamPassed ?? null;
|
||||||
|
|
||||||
const mode = getCourseExitMode(
|
const mode = getCourseExitMode(
|
||||||
certificateData,
|
certificateData,
|
||||||
hasScheduledContent,
|
hasScheduledContent,
|
||||||
@@ -49,6 +54,7 @@ const CertificateStatus = ({ intl }) => {
|
|||||||
userHasPassingGrade,
|
userHasPassingGrade,
|
||||||
null, // CourseExitPageIsActive
|
null, // CourseExitPageIsActive
|
||||||
canViewCertificate,
|
canViewCertificate,
|
||||||
|
entranceExamPassed,
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventProperties = {
|
const eventProperties = {
|
||||||
@@ -208,7 +214,6 @@ const CertificateStatus = ({ intl }) => {
|
|||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!certCase) {
|
if (!certCase) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -236,32 +241,32 @@ const CertificateStatus = ({ intl }) => {
|
|||||||
return (
|
return (
|
||||||
<section data-testid="certificate-status-component" className="text-dark-700 mb-4">
|
<section data-testid="certificate-status-component" className="text-dark-700 mb-4">
|
||||||
<Card className="bg-light-200 raised-card">
|
<Card className="bg-light-200 raised-card">
|
||||||
<Card.Header title={header} />
|
<ProgressCertificateStatusSlot courseId={courseId}>
|
||||||
<Card.Section className="small text-gray-700">
|
<div id={`${certCase}_certificate_status`}>
|
||||||
{body}
|
<Card.Header title={header} />
|
||||||
</Card.Section>
|
<Card.Section className="small text-gray-700">
|
||||||
<Card.Footer>
|
{body}
|
||||||
{buttonText && (buttonLocation || buttonAction) && (
|
</Card.Section>
|
||||||
<Button
|
<Card.Footer>
|
||||||
variant="outline-brand"
|
{buttonText && (buttonLocation || buttonAction) && (
|
||||||
onClick={() => {
|
<Button
|
||||||
logCertificateStatusButtonClicked(certStatus);
|
variant="outline-brand"
|
||||||
if (buttonAction) { buttonAction(); }
|
onClick={() => {
|
||||||
}}
|
logCertificateStatusButtonClicked(certStatus);
|
||||||
href={buttonLocation}
|
if (buttonAction) { buttonAction(); }
|
||||||
block
|
}}
|
||||||
>
|
href={buttonLocation}
|
||||||
{buttonText}
|
block
|
||||||
</Button>
|
>
|
||||||
)}
|
{buttonText}
|
||||||
</Card.Footer>
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card.Footer>
|
||||||
|
</div>
|
||||||
|
</ProgressCertificateStatusSlot>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CertificateStatus.propTypes = {
|
export default CertificateStatus;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CertificateStatus);
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) => {
|
const CompleteDonutSegment = ({ completePercentage, lockedPercentage }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
||||||
|
|
||||||
if (!completePercentage) {
|
if (!completePercentage) {
|
||||||
@@ -82,8 +83,7 @@ const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) =>
|
|||||||
|
|
||||||
CompleteDonutSegment.propTypes = {
|
CompleteDonutSegment.propTypes = {
|
||||||
completePercentage: PropTypes.number.isRequired,
|
completePercentage: PropTypes.number.isRequired,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
lockedPercentage: PropTypes.number.isRequired,
|
lockedPercentage: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CompleteDonutSegment);
|
export default CompleteDonutSegment;
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import React from 'react';
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { useSelector } from 'react-redux';
|
import { useContextId } from '../../../data/hooks';
|
||||||
import {
|
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
import CompleteDonutSegment from './CompleteDonutSegment';
|
import CompleteDonutSegment from './CompleteDonutSegment';
|
||||||
@@ -10,10 +7,9 @@ import IncompleteDonutSegment from './IncompleteDonutSegment';
|
|||||||
import LockedDonutSegment from './LockedDonutSegment';
|
import LockedDonutSegment from './LockedDonutSegment';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CompletionDonutChart = ({ intl }) => {
|
const CompletionDonutChart = () => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
completionSummary: {
|
completionSummary: {
|
||||||
@@ -62,8 +58,4 @@ const CompletionDonutChart = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CompletionDonutChart.propTypes = {
|
export default CompletionDonutChart;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CompletionDonutChart);
|
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import React from 'react';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import CompletionDonutChart from './CompletionDonutChart';
|
import CompletionDonutChart from './CompletionDonutChart';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CourseCompletion = ({ intl }) => (
|
const CourseCompletion = () => {
|
||||||
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
const intl = useIntl();
|
||||||
<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>
|
|
||||||
);
|
|
||||||
|
|
||||||
CourseCompletion.propTypes = {
|
return (
|
||||||
intl: intlShape.isRequired,
|
<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 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 { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
const IncompleteDonutSegment = ({ incompletePercentage }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
||||||
|
|
||||||
if (!incompletePercentage) {
|
if (!incompletePercentage) {
|
||||||
@@ -53,7 +54,6 @@ const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
|||||||
|
|
||||||
IncompleteDonutSegment.propTypes = {
|
IncompleteDonutSegment.propTypes = {
|
||||||
incompletePercentage: PropTypes.number.isRequired,
|
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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
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';
|
import messages from './messages';
|
||||||
|
|
||||||
const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
const LockedDonutSegment = ({ lockedPercentage }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
||||||
|
|
||||||
if (!lockedPercentage) {
|
if (!lockedPercentage) {
|
||||||
@@ -65,8 +66,7 @@ const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
LockedDonutSegment.propTypes = {
|
LockedDonutSegment.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
lockedPercentage: PropTypes.number.isRequired,
|
lockedPercentage: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(LockedDonutSegment);
|
export default LockedDonutSegment;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user