Compare commits
1 Commits
dependabot
...
manwar/VAN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6162174be8 |
7
.env
7
.env
@@ -4,7 +4,6 @@
|
|||||||
NODE_ENV='production'
|
NODE_ENV='production'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME=''
|
ACCESS_TOKEN_COOKIE_NAME=''
|
||||||
APP_ID='learning'
|
|
||||||
BASE_URL=''
|
BASE_URL=''
|
||||||
CONTACT_URL=''
|
CONTACT_URL=''
|
||||||
CREDENTIALS_BASE_URL=''
|
CREDENTIALS_BASE_URL=''
|
||||||
@@ -12,12 +11,11 @@ CREDIT_HELP_LINK_URL=''
|
|||||||
CSRF_TOKEN_API_PATH=''
|
CSRF_TOKEN_API_PATH=''
|
||||||
DISCOVERY_API_BASE_URL=''
|
DISCOVERY_API_BASE_URL=''
|
||||||
DISCUSSIONS_MFE_BASE_URL=''
|
DISCUSSIONS_MFE_BASE_URL=''
|
||||||
DISCOUNT_CODE_INFO_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=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_URL=''
|
|
||||||
EXAMS_BASE_URL=''
|
EXAMS_BASE_URL=''
|
||||||
FAVICON_URL=''
|
FAVICON_URL=''
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
@@ -50,6 +48,3 @@ 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=''
|
|
||||||
# Fallback in local style files
|
|
||||||
PARAGON_THEME_URLS={}
|
|
||||||
|
|||||||
@@ -4,20 +4,18 @@
|
|||||||
NODE_ENV='development'
|
NODE_ENV='development'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
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'
|
||||||
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
DISCOUNT_CODE_INFO_URL=''
|
|
||||||
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'
|
||||||
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
|
|
||||||
EXAMS_BASE_URL=''
|
EXAMS_BASE_URL=''
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
@@ -52,6 +50,3 @@ 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=''
|
|
||||||
# Fallback in local style files
|
|
||||||
PARAGON_THEME_URLS={}
|
|
||||||
|
|||||||
@@ -4,20 +4,18 @@
|
|||||||
NODE_ENV='test'
|
NODE_ENV='test'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
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'
|
||||||
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
DISCOUNT_CODE_INFO_URL=''
|
|
||||||
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'
|
||||||
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
|
|
||||||
EXAMS_BASE_URL='http://localhost:18740'
|
EXAMS_BASE_URL='http://localhost:18740'
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
@@ -49,6 +47,3 @@ 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=''
|
|
||||||
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:Enterprise'
|
|
||||||
FEATURE_ENABLE_CHAT_V2_ENDPOINT='false'
|
|
||||||
|
|||||||
@@ -3,5 +3,3 @@ dist/
|
|||||||
packages/
|
packages/
|
||||||
node_modules/
|
node_modules/
|
||||||
jest.config.js
|
jest.config.js
|
||||||
env.config.jsx
|
|
||||||
example.env.config.jsx
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
const config = createConfig('eslint', {
|
const config = createConfig('eslint', {
|
||||||
rules: {
|
rules: {
|
||||||
@@ -12,13 +12,6 @@ const config = createConfig('eslint', {
|
|||||||
'react/no-unknown-property': 'off',
|
'react/no-unknown-property': 'off',
|
||||||
'func-names': 'off',
|
'func-names': 'off',
|
||||||
},
|
},
|
||||||
settings: {
|
|
||||||
'import/resolver': {
|
|
||||||
webpack: {
|
|
||||||
config: 'webpack.prod.config.js',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -1,7 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
# Adding new check for github-actions
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
18
.github/workflows/add-issue-to-btr-project.yml
vendored
18
.github/workflows/add-issue-to-btr-project.yml
vendored
@@ -1,18 +0,0 @@
|
|||||||
# Run the workflow that adds new tickets that are labelled "release testing"
|
|
||||||
# to the org-wide BTR project board
|
|
||||||
|
|
||||||
name: Add release testing issues to the BTR project board
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
# This workflow is triggered when an issue is labeled with 'release testing'.
|
|
||||||
# It adds the issue to the BTR project and applies the 'needs triage' label
|
|
||||||
# if it doesn't already have it.
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
handle-release-testing:
|
|
||||||
uses: openedx/.github/.github/workflows/add-issue-to-btr-project.yml@master
|
|
||||||
secrets:
|
|
||||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
|
||||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
|
||||||
25
.github/workflows/validate.yml
vendored
25
.github/workflows/validate.yml
vendored
@@ -10,27 +10,14 @@ jobs:
|
|||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Nodejs Env
|
||||||
|
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version: ${{ env.NODE_VER }}
|
||||||
- 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@v6
|
|
||||||
- name: Download code coverage results
|
|
||||||
uses: actions/download-artifact@v5
|
|
||||||
with:
|
|
||||||
pattern: code-coverage-report
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,7 +6,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
coverage
|
coverage
|
||||||
env.config.*
|
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
src/i18n/transifex_input.json
|
src/i18n/transifex_input.json
|
||||||
@@ -26,7 +25,3 @@ module.config.js
|
|||||||
|
|
||||||
# Local environment overrides
|
# Local environment overrides
|
||||||
.env.private
|
.env.private
|
||||||
|
|
||||||
src/i18n/messages/
|
|
||||||
|
|
||||||
env.config.jsx
|
|
||||||
|
|||||||
1
.husky/_/.gitignore
vendored
Normal file
1
.husky/_/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
31
.husky/_/husky.sh
Normal file
31
.husky/_/husky.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/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
|
||||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm run lint
|
||||||
9
.tx/config
Normal file
9
.tx/config
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[main]
|
||||||
|
host = https://www.transifex.com
|
||||||
|
|
||||||
|
[o:open-edx:p:edx-platform:r:frontend-app-learning]
|
||||||
|
file_filter = src/i18n/messages/<lang>.json
|
||||||
|
source_file = src/i18n/transifex_input.json
|
||||||
|
source_lang = en
|
||||||
|
type = KEYVALUEJSON
|
||||||
|
|
||||||
41
Makefile
41
Makefile
@@ -1,17 +1,20 @@
|
|||||||
|
export TRANSIFEX_RESOURCE=frontend-app-learning
|
||||||
|
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fa_IR,fr_CA,it_IT,pt_PT,de_DE"
|
||||||
|
|
||||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||||
i18n = ./src/i18n
|
i18n = ./src/i18n
|
||||||
transifex_input = $(i18n)/transifex_input.json
|
transifex_input = $(i18n)/transifex_input.json
|
||||||
|
|
||||||
# This directory must match .babelrc .
|
# This directory must match .babelrc .
|
||||||
transifex_temp = ./temp/babel-plugin-formatjs
|
transifex_temp = ./temp/babel-plugin-react-intl
|
||||||
|
|
||||||
precommit:
|
precommit:
|
||||||
npm run lint
|
npm run lint
|
||||||
npm audit
|
npm audit
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
npm ci
|
npm install
|
||||||
|
|
||||||
i18n.extract:
|
i18n.extract:
|
||||||
# Pulling display strings from .jsx files into .json files...
|
# Pulling display strings from .jsx files into .json files...
|
||||||
@@ -29,21 +32,37 @@ detect_changed_source_translations:
|
|||||||
# Checking for changed translations...
|
# Checking for changed translations...
|
||||||
git diff --exit-code $(i18n)
|
git diff --exit-code $(i18n)
|
||||||
|
|
||||||
|
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||||
|
push_translations:
|
||||||
|
# Pushing strings to Transifex...
|
||||||
|
tx push -s
|
||||||
|
# Fetching hashes from Transifex...
|
||||||
|
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||||
|
# Writing out comments to file...
|
||||||
|
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||||
|
# Pushing comments to Transifex...
|
||||||
|
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||||
|
|
||||||
|
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||||
|
# Pulls translations from Transifex.
|
||||||
|
pull_translations:
|
||||||
|
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||||
|
else
|
||||||
|
# Experimental: OEP-58 Pulls translations using atlas
|
||||||
pull_translations:
|
pull_translations:
|
||||||
rm -rf src/i18n/messages
|
rm -rf src/i18n/messages
|
||||||
mkdir src/i18n/messages
|
mkdir src/i18n/messages
|
||||||
cd src/i18n/messages \
|
cd src/i18n/messages \
|
||||||
&& atlas pull $(ATLAS_OPTIONS) \
|
&& atlas pull $(ATLAS_OPTIONS) \
|
||||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||||
translations/paragon/src/i18n/messages:paragon \
|
translations/paragon/src/i18n/messages:paragon \
|
||||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||||
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
|
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
|
||||||
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
|
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
|
||||||
|
|
||||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning
|
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning
|
||||||
|
endif
|
||||||
|
|
||||||
# This target is used by Travis.
|
# This target is used by Travis.
|
||||||
validate-no-uncommitted-package-lock-changes:
|
validate-no-uncommitted-package-lock-changes:
|
||||||
@@ -55,10 +74,8 @@ 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:
|
||||||
|
|||||||
133
README.rst
133
README.rst
@@ -1,109 +1,77 @@
|
|||||||
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
|
||||||
=============
|
=============
|
||||||
|
|
||||||
`Tutor`_ is currently recommended as a development environment for the Learning
|
The `devstack`_ is currently recommended as a development environment for your
|
||||||
MFE. Most likely, it already has this MFE configured; however, you'll need to
|
new MFE. If you start it with ``make dev.up.lms`` that should give you
|
||||||
make some changes in order to run it in development mode. You can refer
|
everything you need as a companion to this frontend.
|
||||||
to the `relevant tutor-mfe documentation`_ for details, or follow the quick
|
|
||||||
guide below.
|
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
|
||||||
|
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||||
|
|
||||||
|
.. _Devstack: https://github.com/openedx/devstack
|
||||||
|
|
||||||
.. _Tutor: 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.
|
||||||
|
|
||||||
Cloning and Setup
|
- 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.
|
||||||
=================
|
|
||||||
|
|
||||||
1. Clone your new repo:
|
Cloning and Startup
|
||||||
|
===================
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block::
|
||||||
|
|
||||||
git clone https://github.com/openedx/frontend-app-learning.git
|
1. Clone your new repo:
|
||||||
|
|
||||||
2. Use the version of Node specified in ``.nvmrc``.
|
``git clone https://github.com/openedx/frontend-app-learning.git``
|
||||||
|
|
||||||
Using other major versions of node *may* work, but this is unsupported. For
|
2. Use node v18.x.
|
||||||
convenience, this repository includes an ``.nvmrc`` file to help in setting the
|
|
||||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
|
||||||
|
|
||||||
3. Stop the Tutor devstack, if it's running: ``tutor dev stop``
|
The current version of the micro-frontend build scripts support 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>`_.
|
||||||
|
|
||||||
4. Next, we need to tell Tutor that we're going to be running this repo in
|
3. Install npm dependencies:
|
||||||
development mode, and it should be excluded from the ``mfe`` container that
|
|
||||||
otherwise runs every MFE. Run this:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
``cd frontend-app-learning && npm ci``
|
||||||
|
|
||||||
tutor mounts add /path/to/frontend-app-learning
|
4. Start the dev server:
|
||||||
|
|
||||||
5. Start Tutor in development mode. This command will start the LMS and Studio,
|
``npm start``
|
||||||
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 = {
|
||||||
/*
|
/*
|
||||||
@@ -116,10 +84,10 @@ file (which is git-ignored) that defines where to find your local modules, for i
|
|||||||
may want to use "src" if the module installs React as a peer/dev dependency.
|
may want to use "src" if the module installs React as a peer/dev dependency.
|
||||||
*/
|
*/
|
||||||
localModules: [
|
localModules: [
|
||||||
{ moduleName: '@openedx/paragon/scss', dir: '../paragon', dist: 'scss' },
|
{ moduleName: '@edx/paragon/scss', dir: '../paragon', dist: 'scss' },
|
||||||
{ moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
|
{ moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
|
||||||
{ moduleName: '@openedx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
|
{ moduleName: '@edx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
|
||||||
{ moduleName: '@openedx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
{ moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,21 +98,15 @@ Deployment
|
|||||||
|
|
||||||
The Learning MFE is similar to all the other Open edX MFEs. Read the Open
|
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://openedx.github.io/frontend-platform/>`_.
|
`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,
|
||||||
as documented in the Open edX Developer Guide under
|
as documented in the Open edX Developer Guide under
|
||||||
`Required Environment Variables <https://openedx.github.io/frontend-platform/>`_.
|
`Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`_.
|
||||||
|
|
||||||
The learning micro-frontend also supports the following additional variables:
|
The learning micro-frontend also supports the following additional variables:
|
||||||
|
|
||||||
@@ -165,7 +127,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
|
||||||
|
|
||||||
@@ -178,7 +140,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
|
||||||
@@ -194,13 +156,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/openedx
|
Example: https://twitter.com/edXOnline
|
||||||
|
|
||||||
Getting Help
|
Getting Help
|
||||||
============
|
===========
|
||||||
|
|
||||||
If you're having trouble, we have `discussion forums`_
|
If you're having trouble, we have discussion forums at
|
||||||
where you can connect with others in the community.
|
https://discuss.openedx.org 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
|
||||||
@@ -218,18 +180,17 @@ 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 discuss your new feature idea with the maintainers before
|
to have a discussion about your new feature idea with the maintainers prior to
|
||||||
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.
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# This file records information about this repo. Its use is described in OEP-55:
|
|
||||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
|
||||||
|
|
||||||
apiVersion: backstage.io/v1alpha1
|
|
||||||
kind: Component
|
|
||||||
metadata:
|
|
||||||
name: 'frontend-app-learning'
|
|
||||||
description: "This is the Learning MFE, which renders all learner-facing course pages."
|
|
||||||
links:
|
|
||||||
- url: "https://github.com/openedx/frontend-app-learning"
|
|
||||||
title: "Learning MFE"
|
|
||||||
icon: "Web"
|
|
||||||
annotations:
|
|
||||||
openedx.org/arch-interest-groups: ""
|
|
||||||
openedx.org/release: "master"
|
|
||||||
spec:
|
|
||||||
owner: group:committers-frontend-app-learning
|
|
||||||
type: 'website'
|
|
||||||
lifecycle: 'production'
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import UnitTranslationPlugin from '@edx/unit-translation-selector-plugin';
|
|
||||||
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
|
|
||||||
|
|
||||||
// Load environment variables from .env file
|
|
||||||
const config = {
|
|
||||||
...process.env,
|
|
||||||
pluginSlots: {
|
|
||||||
unit_title_plugin: {
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
op: PLUGIN_OPERATIONS.Insert,
|
|
||||||
widget: {
|
|
||||||
id: 'unit_title_plugin',
|
|
||||||
type: DIRECT_PLUGIN,
|
|
||||||
priority: 1,
|
|
||||||
RenderWidget: UnitTranslationPlugin,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
const config = createConfig('jest', {
|
module.exports = createConfig('jest', {
|
||||||
setupFilesAfterEnv: [
|
setupFilesAfterEnv: [
|
||||||
'<rootDir>/src/setupTest.js',
|
'<rootDir>/src/setupTest.js',
|
||||||
],
|
],
|
||||||
@@ -9,35 +9,13 @@ 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',
|
|
||||||
// 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",
|
|
||||||
verbose: true,
|
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
|
globalSetup: "./global-setup.js"
|
||||||
});
|
});
|
||||||
|
|
||||||
// delete config.testURL;
|
|
||||||
|
|
||||||
config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
|
|
||||||
// change this setting if need to see less details for each test
|
|
||||||
// reportType: "summary" | "details",
|
|
||||||
// enable: true | false,
|
|
||||||
afterEachTest: {
|
|
||||||
enable: true,
|
|
||||||
filePaths: false,
|
|
||||||
reportType: "details",
|
|
||||||
},
|
|
||||||
afterAllTests: {
|
|
||||||
reportType: "summary",
|
|
||||||
enable: true,
|
|
||||||
filePaths: true,
|
|
||||||
},
|
|
||||||
}]];
|
|
||||||
|
|
||||||
module.exports = config;
|
|
||||||
|
|||||||
10
openedx.yaml
Normal file
10
openedx.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 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
|
||||||
23056
package-lock.json
generated
23056
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
89
package.json
89
package.json
@@ -11,16 +11,13 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "fedx-scripts webpack",
|
||||||
"bundlewatch": "bundlewatch",
|
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||||
"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",
|
||||||
|
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"start": "fedx-scripts webpack-dev-server --progress",
|
||||||
"start:with-theme": "paragon install-theme && npm start && npm install",
|
"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": "NODE_ENV=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",
|
||||||
@@ -33,66 +30,56 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/browserslist-config": "1.5.0",
|
"@edx/frontend-component-footer": "12.2.1",
|
||||||
"@edx/frontend-component-footer": "^14.6.0",
|
"@edx/frontend-component-header": "4.6.0",
|
||||||
"@edx/frontend-component-header": "^8.0.0",
|
"@edx/frontend-lib-special-exams": "2.27.0",
|
||||||
"@edx/frontend-lib-learning-assistant": "^2.24.0",
|
"@edx/frontend-lib-learning-assistant": "^1.20.1",
|
||||||
"@edx/frontend-lib-special-exams": "^4.0.0",
|
"@edx/frontend-platform": "5.5.2",
|
||||||
"@edx/frontend-platform": "^8.4.0",
|
"@edx/openedx-atlas": "^0.6.0",
|
||||||
"@edx/openedx-atlas": "^0.7.0",
|
"@openedx/paragon": "22.0.0",
|
||||||
|
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.7.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-build": "^14.6.2",
|
|
||||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
|
||||||
"@openedx/paragon": "^23.4.5",
|
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@reduxjs/toolkit": "1.9.7",
|
"@reduxjs/toolkit": "1.8.1",
|
||||||
"buffer": "^6.0.3",
|
"classnames": "2.3.2",
|
||||||
"classnames": "2.5.1",
|
"core-js": "3.22.2",
|
||||||
"copy-webpack-plugin": "^12.0.0",
|
"history": "5.3.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",
|
||||||
"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": "^18.3.1",
|
"react": "17.0.2",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "17.0.2",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"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.2.1",
|
"redux": "4.1.2",
|
||||||
|
"regenerator-runtime": "0.13.11",
|
||||||
"reselect": "4.1.8",
|
"reselect": "4.1.8",
|
||||||
"sass": "^1.79.3",
|
"truncate-html": "1.0.4",
|
||||||
"sass-loader": "^16.0.2",
|
"util": "0.12.5"
|
||||||
"source-map-loader": "^5.0.0",
|
|
||||||
"truncate-html": "1.0.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pact-foundation/pact": "^13.0.0",
|
"@edx/browserslist-config": "1.2.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@edx/frontend-build": "^12.9.10",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@edx/reactifex": "2.2.0",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@pact-foundation/pact": "^11.0.2",
|
||||||
"axios-mock-adapter": "2.1.0",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"bundlewatch": "^0.4.0",
|
"@testing-library/react": "12.1.5",
|
||||||
"eslint-import-resolver-webpack": "^0.13.9",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
"jest": "^29.7.0",
|
"@testing-library/user-event": "13.5.0",
|
||||||
"jest-console-group-reporter": "^1.1.1",
|
"axios-mock-adapter": "1.20.0",
|
||||||
"jest-when": "^3.6.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"es-check": "6.2.1",
|
||||||
|
"husky": "7.0.4",
|
||||||
|
"jest": "29.5.0",
|
||||||
"rosie": "2.1.1"
|
"rosie": "2.1.1"
|
||||||
},
|
|
||||||
"bundlewatch": {
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"path": "dist/*.js",
|
|
||||||
"maxSize": "1450kB"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,6 @@
|
|||||||
<% 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,13 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { FormattedMessage, FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
import {
|
||||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
FormattedMessage, FormattedDate, injectIntl, intlShape,
|
||||||
import { Info } from '@openedx/paragon/icons';
|
} from '@edx/frontend-platform/i18n';
|
||||||
|
import { Alert, Hyperlink } from '@edx/paragon';
|
||||||
|
import { Info } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AccessExpirationAlert = ({ payload }) => {
|
const AccessExpirationAlert = ({ intl, payload }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
courseId,
|
courseId,
|
||||||
@@ -118,6 +119,7 @@ const AccessExpirationAlert = ({ payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AccessExpirationAlert.propTypes = {
|
AccessExpirationAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
accessExpiration: PropTypes.shape({
|
accessExpiration: PropTypes.shape({
|
||||||
expirationDate: PropTypes.string.isRequired,
|
expirationDate: PropTypes.string.isRequired,
|
||||||
@@ -132,4 +134,4 @@ AccessExpirationAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AccessExpirationAlert;
|
export default injectIntl(AccessExpirationAlert);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||||
import { PageBanner } from '@openedx/paragon';
|
import { PageBanner } from '@edx/paragon';
|
||||||
|
|
||||||
const AccessExpirationMasqueradeBanner = ({ payload }) => {
|
const AccessExpirationMasqueradeBanner = ({ payload }) => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
import { Alert, Hyperlink } from '@edx/paragon';
|
||||||
import { WarningFilled } from '@openedx/paragon/icons';
|
import { WarningFilled } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import genericMessages from './messages';
|
import genericMessages from './messages';
|
||||||
|
|
||||||
const ActiveEnterpriseAlert = ({ payload }) => {
|
const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const { text, courseId } = payload;
|
const { text, courseId } = payload;
|
||||||
const changeActiveEnterprise = (
|
const changeActiveEnterprise = (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
@@ -39,10 +38,11 @@ const ActiveEnterpriseAlert = ({ payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ActiveEnterpriseAlert.propTypes = {
|
ActiveEnterpriseAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActiveEnterpriseAlert;
|
export default injectIntl(ActiveEnterpriseAlert);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
FormattedRelativeTime,
|
FormattedRelativeTime,
|
||||||
FormattedTime,
|
FormattedTime,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@openedx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
import { Info } from '@openedx/paragon/icons';
|
import { Info } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||||
import { PageBanner } from '@openedx/paragon';
|
import { PageBanner } from '@edx/paragon';
|
||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Alert, Button } from '@openedx/paragon';
|
import { Alert, Button } from '@edx/paragon';
|
||||||
import { Info, WarningFilled } from '@openedx/paragon/icons';
|
import { Info, WarningFilled } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||||
@@ -11,8 +11,7 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import useEnrollClickHandler from './clickHook';
|
import useEnrollClickHandler from './clickHook';
|
||||||
|
|
||||||
const EnrollmentAlert = ({ payload }) => {
|
const EnrollmentAlert = ({ intl, payload }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
canEnroll,
|
canEnroll,
|
||||||
courseId,
|
courseId,
|
||||||
@@ -59,6 +58,7 @@ const EnrollmentAlert = ({ payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
EnrollmentAlert.propTypes = {
|
EnrollmentAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
canEnroll: PropTypes.bool,
|
canEnroll: PropTypes.bool,
|
||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
@@ -67,4 +67,4 @@ EnrollmentAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EnrollmentAlert;
|
export default injectIntl(EnrollmentAlert);
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
Icon,
|
Icon,
|
||||||
} from '@openedx/paragon';
|
} from '@edx/paragon';
|
||||||
import { Check, ArrowForward } from '@openedx/paragon/icons';
|
import { Check, ArrowForward } from '@edx/paragon/icons';
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { sendActivationEmail } from '../../courseware/data';
|
import { sendActivationEmail } from '../../courseware/data';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AccountActivationAlert = () => {
|
const AccountActivationAlert = ({
|
||||||
const intl = useIntl();
|
intl,
|
||||||
|
}) => {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showSpinner, setShowSpinner] = useState(false);
|
const [showSpinner, setShowSpinner] = useState(false);
|
||||||
const [showCheck, setShowCheck] = useState(false);
|
const [showCheck, setShowCheck] = useState(false);
|
||||||
@@ -124,4 +125,8 @@ const AccountActivationAlert = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AccountActivationAlert;
|
AccountActivationAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(AccountActivationAlert);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
import { Alert, Hyperlink } from '@edx/paragon';
|
||||||
import { WarningFilled } from '@openedx/paragon/icons';
|
import { WarningFilled } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import genericMessages from '../../generic/messages';
|
import genericMessages from '../../generic/messages';
|
||||||
|
|
||||||
const LogistrationAlert = () => {
|
const LogistrationAlert = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const signIn = (
|
const signIn = (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
style={{ textDecoration: 'underline' }}
|
style={{ textDecoration: 'underline' }}
|
||||||
@@ -44,4 +43,8 @@ const LogistrationAlert = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LogistrationAlert;
|
LogistrationAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(LogistrationAlert);
|
||||||
|
|||||||
35
src/constants.js
Normal file
35
src/constants.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
@@ -1,74 +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',
|
|
||||||
'/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;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Tabs, Tab } from '@openedx/paragon';
|
import { Tabs, Tab } from '@edx/paragon';
|
||||||
|
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import CoursewareSearchResults from './CoursewareSearchResults';
|
import CoursewareSearchResults from './CoursewareSearchResults';
|
||||||
@@ -8,13 +8,17 @@ import messages from './messages';
|
|||||||
import { useCoursewareSearchParams } from './hooks';
|
import { useCoursewareSearchParams } from './hooks';
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
const filterAll = 'all';
|
const allFilterKey = 'all';
|
||||||
const filterTypes = ['text', 'video', 'sequence'];
|
const otherFilterKey = 'other';
|
||||||
const filterOther = 'other';
|
const allowedFilterKeys = {
|
||||||
const validFilters = [filterAll, ...filterTypes, filterOther];
|
[allFilterKey]: true,
|
||||||
|
text: true,
|
||||||
|
video: true,
|
||||||
|
sequence: true,
|
||||||
|
[otherFilterKey]: true,
|
||||||
|
};
|
||||||
|
|
||||||
export const CoursewareSearchResultsFilter = () => {
|
export const CoursewareSearchResultsFilter = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const lastSearch = useModel('contentSearchResults', courseId);
|
const lastSearch = useModel('contentSearchResults', courseId);
|
||||||
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
|
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
|
||||||
@@ -23,38 +27,23 @@ export const CoursewareSearchResultsFilter = () => {
|
|||||||
|
|
||||||
const { results: data = [] } = lastSearch;
|
const { results: data = [] } = lastSearch;
|
||||||
|
|
||||||
// If there's no data, we show an empty result.
|
const results = useMemo(() => data.reduce((acc, { type, ...rest }) => {
|
||||||
if (!data.length) { return <CoursewareSearchResults />; }
|
acc[allFilterKey] = [...(acc[allFilterKey] || []), { type: allFilterKey, ...rest }];
|
||||||
|
if (type === allFilterKey) { return acc; }
|
||||||
|
|
||||||
const results = useMemo(() => {
|
let targetKey = otherFilterKey;
|
||||||
// This reducer distributes the data into different groups to make it easy to
|
if (allowedFilterKeys[type]) { targetKey = type; }
|
||||||
// use on the filters.
|
acc[targetKey] = [...(acc[targetKey] || []), { type: targetKey, ...rest }];
|
||||||
// All results are added to the "all" key and then to its proper group key as well.
|
return acc;
|
||||||
const grouped = data.reduce((acc, { type, ...rest }) => {
|
}, {}), [data]);
|
||||||
const resultType = filterTypes.includes(type) ? type : filterOther;
|
|
||||||
acc[filterAll].push({ type: resultType, ...rest });
|
|
||||||
acc[resultType] = [...(acc[resultType] || []), { type: resultType, ...rest }];
|
|
||||||
return acc;
|
|
||||||
}, { [filterAll]: [] });
|
|
||||||
|
|
||||||
// This is just to format the output object with the expected tab order.
|
const filters = useMemo(() => Object.keys(allowedFilterKeys).map((key) => ({
|
||||||
const output = {};
|
|
||||||
validFilters.forEach(key => { if (grouped[key]) { output[key] = grouped[key]; } });
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}, [lastSearch]);
|
|
||||||
|
|
||||||
const tabKeys = Object.keys(results);
|
|
||||||
// Filter has no use if it has only 2 tabs (The "all" tab and another one with the same items).
|
|
||||||
if (tabKeys.length < 3) { return <CoursewareSearchResults results={results[filterAll]} />; }
|
|
||||||
|
|
||||||
const filters = useMemo(() => tabKeys.map((key) => ({
|
|
||||||
key,
|
key,
|
||||||
label: intl.formatMessage(messages[`filter:${key}`]),
|
label: intl.formatMessage(messages[`filter:${key}`]),
|
||||||
count: results[key].length,
|
count: results[key]?.length || 0,
|
||||||
})), [results]);
|
})), [results]);
|
||||||
|
|
||||||
const activeKey = validFilters.includes(filterKeyword) ? filterKeyword : filterAll;
|
const activeKey = allowedFilterKeys[filterKeyword] ? filterKeyword : allFilterKey;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -65,7 +54,7 @@ export const CoursewareSearchResultsFilter = () => {
|
|||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onSelect={setFilter}
|
onSelect={setFilter}
|
||||||
>
|
>
|
||||||
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
|
{filters.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}`}>
|
||||||
<CoursewareSearchResults results={results[key]} />
|
<CoursewareSearchResults results={results[key]} />
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -74,4 +63,8 @@ export const CoursewareSearchResultsFilter = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CoursewareSearchResultsFilter;
|
CoursewareSearchResultsFilter.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CoursewareSearchResultsFilter);
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ jest.mock('../../generic/model-store', () => ({
|
|||||||
useModel: jest.fn(),
|
useModel: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
|
||||||
observe: jest.fn(),
|
|
||||||
unobserve: jest.fn(),
|
|
||||||
disconnect: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
|
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
|
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
|
||||||
const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc';
|
const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc';
|
||||||
@@ -58,75 +52,49 @@ function renderComponent(props = {}) {
|
|||||||
describe('CoursewareSearchResultsFilter', () => {
|
describe('CoursewareSearchResultsFilter', () => {
|
||||||
beforeAll(initializeMockApp);
|
beforeAll(initializeMockApp);
|
||||||
|
|
||||||
beforeEach(() => {
|
describe('</CoursewareSearchResultsFilter />', () => {
|
||||||
useCoursewareSearchParams.mockReturnValue(coursewareSearch);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when returning full results', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useModel.mockReturnValue(searchResultsFactory());
|
useCoursewareSearchParams.mockReturnValue(coursewareSearch);
|
||||||
renderComponent();
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render without errors', async () => {
|
it('should render without errors', async () => {
|
||||||
await waitFor(() => {
|
useModel.mockReturnValue(searchResultsFactory());
|
||||||
expect(useCoursewareSearchParams).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when returning only one result type', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
// Get results for only videos
|
|
||||||
const data = searchResultsFactory();
|
|
||||||
const onlyVideos = data.results.filter(({ type }) => type === 'video');
|
|
||||||
const filteredResults = {
|
|
||||||
...data,
|
|
||||||
results: onlyVideos,
|
|
||||||
};
|
|
||||||
|
|
||||||
useModel.mockReturnValue(filteredResults);
|
|
||||||
await renderComponent();
|
await renderComponent();
|
||||||
});
|
|
||||||
|
|
||||||
it('should not render', async () => {
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(useCoursewareSearchParams).toBeCalled();
|
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when there are not results', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
useModel.mockReturnValue(searchResultsFactory('blah', {
|
|
||||||
results: [],
|
|
||||||
filters: [],
|
|
||||||
total: 0,
|
|
||||||
maxScore: null,
|
|
||||||
ms: 5,
|
|
||||||
}));
|
|
||||||
await renderComponent();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render', async () => {
|
describe('when there are not results', () => {
|
||||||
await waitFor(() => {
|
it('should render without errors', async () => {
|
||||||
expect(useCoursewareSearchParams).toBeCalled();
|
useModel.mockReturnValue(searchResultsFactory('blah', {
|
||||||
});
|
results: [],
|
||||||
|
filters: [],
|
||||||
|
total: 0,
|
||||||
|
maxScore: null,
|
||||||
|
ms: 5,
|
||||||
|
}));
|
||||||
|
|
||||||
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
|
await renderComponent();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect } 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 { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Alert, Button, Icon, Spinner,
|
Alert, Button, Icon, Spinner,
|
||||||
} from '@openedx/paragon';
|
} from '@edx/paragon';
|
||||||
import {
|
import {
|
||||||
Close,
|
Close,
|
||||||
} from '@openedx/paragon/icons';
|
} from '@edx/paragon/icons';
|
||||||
import { setShowSearch } from '../data/slice';
|
import { setShowSearch } from '../data/slice';
|
||||||
import { useCoursewareSearchParams, useElementBoundingBox, useLockScroll } from './hooks';
|
import { useCoursewareSearchParams, useElementBoundingBox, useLockScroll } from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -18,8 +18,7 @@ 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 = ({ ...sectionProps }) => {
|
const CoursewareSearch = ({ intl, ...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();
|
||||||
@@ -30,7 +29,6 @@ const CoursewareSearch = ({ ...sectionProps }) => {
|
|||||||
errors,
|
errors,
|
||||||
total,
|
total,
|
||||||
} = useModel('contentSearchResults', courseId);
|
} = useModel('contentSearchResults', courseId);
|
||||||
const dialogRef = useRef();
|
|
||||||
|
|
||||||
useLockScroll();
|
useLockScroll();
|
||||||
|
|
||||||
@@ -46,8 +44,7 @@ const CoursewareSearch = ({ ...sectionProps }) => {
|
|||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
results: [],
|
results: [],
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
loading:
|
loading: false,
|
||||||
false,
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -69,46 +66,20 @@ const CoursewareSearch = ({ ...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 close = () => {
|
const handleSearchCloseClick = () => {
|
||||||
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';
|
||||||
@@ -119,58 +90,59 @@ const CoursewareSearch = ({ ...sectionProps }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog ref={dialogRef} className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-dialog" onClose={handleSearchClose} {...sectionProps}>
|
<section className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-section" {...sectionProps}>
|
||||||
|
<div className="courseware-search__close">
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
className="p-1"
|
||||||
|
aria-label={intl.formatMessage(messages.searchCloseAction)}
|
||||||
|
onClick={handleSearchCloseClick}
|
||||||
|
data-testid="courseware-search-close-button"
|
||||||
|
><Icon src={Close} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="courseware-search__outer-content">
|
<div className="courseware-search__outer-content">
|
||||||
<div className="courseware-search__content" data-testid="courseware-search-content">
|
<div className="courseware-search__content">
|
||||||
<div className="courseware-search__form">
|
<h1 class="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1>
|
||||||
<h1 className="h2">{formatMessage(messages.searchModuleTitle)}</h1>
|
<CoursewareSearchForm
|
||||||
<CoursewareSearchForm
|
searchTerm={searchKeyword}
|
||||||
searchTerm={searchKeyword}
|
onSubmit={handleSubmit}
|
||||||
onSubmit={handleSubmit}
|
onChange={handleOnChange}
|
||||||
onChange={handleOnChange}
|
placeholder={intl.formatMessage(messages.searchBarPlaceholderText)}
|
||||||
placeholder={formatMessage(messages.searchBarPlaceholderText)}
|
/>
|
||||||
/>
|
{status === 'loading' ? (
|
||||||
<div className="courseware-search__close">
|
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
||||||
<Button
|
<Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} />
|
||||||
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>
|
||||||
</div>
|
) : null}
|
||||||
<div className="courseware-search__results" aria-live="polite" data-testid="courseware-search-results">
|
{status === 'error' && (
|
||||||
{status === 'loading' ? (
|
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
||||||
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
{intl.formatMessage(messages.searchResultsError)}
|
||||||
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
|
</Alert>
|
||||||
|
)}
|
||||||
|
{status === 'results' ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="courseware-search__results-summary"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="all"
|
||||||
|
aria-atomic="true"
|
||||||
|
data-testid="courseware-search-summary"
|
||||||
|
>{total > 0
|
||||||
|
? intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })
|
||||||
|
: intl.formatMessage(messages.searchResultsNone)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<CoursewareSearchResultsFilterContainer />
|
||||||
{status === 'error' && (
|
</>
|
||||||
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
) : null}
|
||||||
{formatMessage(messages.searchResultsError)}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{status === 'results' ? (
|
|
||||||
<>
|
|
||||||
{total > 0 ? (
|
|
||||||
<div
|
|
||||||
className="courseware-search__results-summary"
|
|
||||||
aria-relevant="all"
|
|
||||||
aria-atomic="true"
|
|
||||||
data-testid="courseware-search-summary"
|
|
||||||
>{formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<CoursewareSearchResultsFilterContainer />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CoursewareSearch;
|
CoursewareSearch.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CoursewareSearch);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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';
|
||||||
@@ -20,7 +19,6 @@ 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(),
|
||||||
}));
|
}));
|
||||||
@@ -58,7 +56,7 @@ const defaultProps = {
|
|||||||
total: 0,
|
total: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultSearchParams = {
|
const coursewareSearch = {
|
||||||
query: '',
|
query: '',
|
||||||
filter: '',
|
filter: '',
|
||||||
setQuery: jest.fn(),
|
setQuery: jest.fn(),
|
||||||
@@ -98,20 +96,14 @@ const mockModels = ((props = defaultProps) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockSearchParams = ((params) => {
|
const mockSearchParams = ((props = coursewareSearch) => {
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,22 +113,27 @@ describe('CoursewareSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
|
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
|
||||||
|
mockModels();
|
||||||
|
mockSearchParams();
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
expect(useElementBoundingBox).toHaveBeenCalledTimes(1);
|
expect(useElementBoundingBox).toBeCalledTimes(1);
|
||||||
expect(useLockScroll).toHaveBeenCalledTimes(1);
|
expect(useLockScroll).toBeCalledTimes(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-dialog');
|
const section = screen.getByTestId('courseware-search-section');
|
||||||
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 close the dialog', async () => {
|
it('should dispatch setShowSearch(false)', async () => {
|
||||||
|
mockModels();
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -144,8 +141,7 @@ describe('CoursewareSearch', () => {
|
|||||||
fireEvent.click(close);
|
fireEvent.click(close);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(HTMLDialogElement.prototype.close).toHaveBeenCalled();
|
expect(setShowSearch).toBeCalledWith(false);
|
||||||
expect(setShowSearch).toHaveBeenCalledWith(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,24 +149,29 @@ 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-dialog');
|
const section = screen.getByTestId('courseware-search-section');
|
||||||
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-dialog');
|
const section = screen.getByTestId('courseware-search-section');
|
||||||
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(() => {
|
||||||
@@ -202,6 +203,7 @@ describe('CoursewareSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchCourseContent', async () => {
|
it('should call searchCourseContent', async () => {
|
||||||
|
mockModels();
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
const searchKeyword = 'course';
|
const searchKeyword = 'course';
|
||||||
@@ -234,33 +236,29 @@ describe('CoursewareSearch', () => {
|
|||||||
expect(screen.queryByTestId('courseware-search-error')).toBeInTheDocument();
|
expect(screen.queryByTestId('courseware-search-error')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show a summary if there are no results', () => {
|
it('should show "No results found." if results is empty', () => {
|
||||||
mockModels({
|
mockModels({
|
||||||
searchKeyword: 'test',
|
searchKeyword: 'test',
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('No results found.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show a summary for the results within a container with aria-live="polite"', () => {
|
it('should show a summary for the results', () => {
|
||||||
mockModels({
|
mockModels({
|
||||||
searchKeyword: 'fubar',
|
searchKeyword: 'fubar',
|
||||||
total: 1,
|
total: 1,
|
||||||
});
|
});
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
const results = screen.queryByTestId('courseware-search-results');
|
expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
|
||||||
|
|
||||||
expect(results).toHaveAttribute('aria-live', 'polite');
|
|
||||||
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,14 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CoursewareSearchEmpty = () => {
|
const CoursewareSearchEmpty = ({ intl }) => (
|
||||||
const intl = useIntl();
|
<div className="courseware-search-results">
|
||||||
return (
|
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
||||||
<div className="courseware-search-results">
|
</div>
|
||||||
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
);
|
||||||
</div>
|
|
||||||
);
|
CoursewareSearchEmpty.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CoursewareSearchEmpty;
|
export default injectIntl(CoursewareSearchEmpty);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
screen,
|
screen,
|
||||||
} from '../../setupTest';
|
} from '../../setupTest';
|
||||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
function renderComponent() {
|
function renderComponent() {
|
||||||
const { container } = render(<CoursewareSearchEmpty />);
|
const { container } = render(<CoursewareSearchEmpty />);
|
||||||
@@ -17,12 +16,9 @@ describe('CoursewareSearchEmpty', () => {
|
|||||||
initializeMockApp();
|
initializeMockApp();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render empty results text and corresponding classes', () => {
|
it('should match the snapshot', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
const emptyText = screen.getByText(messages.searchResultsNone.defaultMessage);
|
|
||||||
expect(emptyText).toBeInTheDocument();
|
expect(screen.getByTestId('no-results')).toMatchSnapshot();
|
||||||
expect(emptyText).toHaveClass('courseware-search-results__empty');
|
|
||||||
expect(emptyText).toHaveAttribute('data-testid', 'no-results');
|
|
||||||
expect(emptyText.parentElement).toHaveClass('courseware-search-results');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,44 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SearchField } from '@openedx/paragon';
|
import { SearchField } from '@edx/paragon';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } 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,
|
||||||
}) => {
|
}) => (
|
||||||
const { formatMessage } = useIntl();
|
<SearchField.Advanced
|
||||||
|
value={searchTerm}
|
||||||
return (
|
onSubmit={onSubmit}
|
||||||
<SearchField.Advanced
|
onChange={onChange}
|
||||||
value={searchTerm}
|
submitButtonLocation="external"
|
||||||
onSubmit={onSubmit}
|
className="courseware-search-form"
|
||||||
onChange={onChange}
|
screenReaderText={{
|
||||||
|
label: intl.formatMessage(messages.searchSubmitLabel),
|
||||||
|
clearButton: intl.formatMessage(messages.searchClearAction),
|
||||||
|
submitButton: null, // Remove the sr-only label in the button.
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="pgn__searchfield_wrapper" data-testid="courseware-search-form">
|
||||||
|
<SearchField.Label />
|
||||||
|
<SearchField.Input placeholder={placeholder} autoFocus />
|
||||||
|
<SearchField.ClearButton />
|
||||||
|
</div>
|
||||||
|
<SearchField.SubmitButton
|
||||||
|
buttonText={intl.formatMessage(messages.searchSubmitLabel)}
|
||||||
submitButtonLocation="external"
|
submitButtonLocation="external"
|
||||||
className="courseware-search-form"
|
data-testid="courseware-search-form-submit"
|
||||||
screenReaderText={{
|
/>
|
||||||
label: formatMessage(messages.searchSubmitLabel),
|
</SearchField.Advanced>
|
||||||
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,
|
||||||
@@ -52,4 +51,4 @@ CoursewareSearchForm.defaultProps = {
|
|||||||
placeholder: undefined,
|
placeholder: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CoursewareSearchForm;
|
export default injectIntl(CoursewareSearchForm);
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Folder, TextFields, VideoCamera, Article,
|
Folder, TextFields, VideoCamera, Article,
|
||||||
} from '@openedx/paragon/icons';
|
} from '@edx/paragon/icons';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { Icon } from '@openedx/paragon';
|
import { Icon } from '@edx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||||
|
|
||||||
const iconTypeMapping = {
|
const iconTypeMapping = {
|
||||||
|
document: Folder,
|
||||||
text: TextFields,
|
text: TextFields,
|
||||||
video: VideoCamera,
|
video: VideoCamera,
|
||||||
sequence: Folder,
|
|
||||||
other: Article,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultIcon = Article;
|
const defaultIcon = Article;
|
||||||
@@ -35,7 +34,9 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
|||||||
}) => {
|
}) => {
|
||||||
const key = type.toLowerCase();
|
const key = type.toLowerCase();
|
||||||
const icon = iconTypeMapping[key] || defaultIcon;
|
const icon = iconTypeMapping[key] || defaultIcon;
|
||||||
|
|
||||||
const isExternal = !url.startsWith('/');
|
const isExternal = !url.startsWith('/');
|
||||||
|
|
||||||
const linkProps = isExternal ? {
|
const linkProps = isExternal ? {
|
||||||
href: url,
|
href: url,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import CoursewareSearchResults from './CoursewareSearchResults';
|
import CoursewareSearchResults from './CoursewareSearchResults';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import searchResultsFactory from './test-data/search-results-factory';
|
import searchResultsFactory from './test-data/search-results-factory';
|
||||||
import * as mock from './test-data/mocked-response.json';
|
|
||||||
|
|
||||||
jest.mock('react-redux');
|
jest.mock('react-redux');
|
||||||
|
|
||||||
@@ -35,53 +34,8 @@ describe('CoursewareSearchResults', () => {
|
|||||||
renderComponent({ results });
|
renderComponent({ results });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render complete list', () => {
|
it('should match the snapshot', () => {
|
||||||
const courses = screen.getAllByRole('link');
|
expect(screen.getByTestId('search-results')).toMatchSnapshot();
|
||||||
expect(courses.length).toBe(mock.results.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render correct link for internal course', () => {
|
|
||||||
const courses = screen.getAllByRole('link');
|
|
||||||
const firstCourse = courses[0];
|
|
||||||
const firstCourseTitle = firstCourse.querySelector('.courseware-search-results__title span');
|
|
||||||
expect(firstCourseTitle.innerHTML).toEqual(mock.results[0].data.content.display_name);
|
|
||||||
expect(firstCourse.href).toContain(mock.results[0].data.url);
|
|
||||||
expect(firstCourse).not.toHaveAttribute('target', '_blank');
|
|
||||||
expect(firstCourse).not.toHaveAttribute('rel', 'nofollow');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render correct link if is External url course', () => {
|
|
||||||
const courses = screen.getAllByRole('link');
|
|
||||||
const externalCourse = courses[courses.length - 1];
|
|
||||||
const externalCourseTitle = externalCourse.querySelector('.courseware-search-results__title span');
|
|
||||||
expect(externalCourseTitle.innerHTML).toEqual(mock.results[mock.results.length - 1].data.content.display_name);
|
|
||||||
expect(externalCourse.href).toContain(mock.results[mock.results.length - 1].data.url);
|
|
||||||
expect(externalCourse).toHaveAttribute('target', '_blank');
|
|
||||||
expect(externalCourse).toHaveAttribute('rel', 'nofollow');
|
|
||||||
const icon = externalCourse.querySelector('svg');
|
|
||||||
expect(icon).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render location breadcrumbs', () => {
|
|
||||||
const breadcrumbs = screen.getAllByText(mock.results[0].data.location[0]);
|
|
||||||
expect(breadcrumbs.length).toBeGreaterThan(0);
|
|
||||||
const firstBreadcrumb = breadcrumbs[0].closest('li');
|
|
||||||
expect(firstBreadcrumb).toBeInTheDocument();
|
|
||||||
expect(firstBreadcrumb.querySelector('div').textContent).toBe(mock.results[0].data.location[0]);
|
|
||||||
expect(firstBreadcrumb.nextSibling.querySelector('div').textContent).toBe(mock.results[0].data.location[1]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when results are provided with content hits', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const { results } = searchResultsFactory('Passing');
|
|
||||||
renderComponent({ results });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render content hits', () => {
|
|
||||||
const contentHits = screen.getByText('1');
|
|
||||||
expect(contentHits).toBeInTheDocument();
|
|
||||||
expect(contentHits.tagName).toBe('EM');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@openedx/paragon';
|
import { Button, Icon } from '@edx/paragon';
|
||||||
import { ManageSearch } from '@openedx/paragon/icons';
|
import { Search } from '@edx/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';
|
||||||
import { setShowSearch } from '../data/slice';
|
import { setShowSearch } from '../data/slice';
|
||||||
|
|
||||||
const CoursewareSearchToggle = () => {
|
const CoursewareSearchToggle = ({
|
||||||
const intl = useIntl();
|
intl,
|
||||||
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const enabled = useCoursewareSearchFeatureFlag();
|
const enabled = useCoursewareSearchFeatureFlag();
|
||||||
const { query } = useCoursewareSearchParams();
|
const { query } = useCoursewareSearchParams();
|
||||||
@@ -24,20 +25,23 @@ const CoursewareSearchToggle = () => {
|
|||||||
if (!enabled) { return null; }
|
if (!enabled) { return null; }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="courseware-search-toggle">
|
<div className="courseware-searc-toggle">
|
||||||
<Button
|
<Button
|
||||||
variant="outline-primary"
|
variant="tertiary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="p-1 mt-2 mr-2"
|
className="p-1 mt-2 mr-2 rounded-lg"
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.contentSearchButton)}
|
<Icon src={Search} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CoursewareSearchToggle;
|
CoursewareSearchToggle.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CoursewareSearchToggle);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CoursewareSearchEmpty should match the snapshot 1`] = `
|
||||||
|
<p
|
||||||
|
class="courseware-search-results__empty"
|
||||||
|
data-testid="no-results"
|
||||||
|
>
|
||||||
|
No results found.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,1238 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
class="courseware-search-results"
|
||||||
|
data-testid="search-results"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Demo Course Overview
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Introduction
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Demo Course Overview
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Passing a Course
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
1
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
About Exams and Certificates
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
edX Exams
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Passing a Course
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Passing a Course
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
About Exams and Certificates
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
edX Exams
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Passing a Course
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Text Input
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 1: Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Question Styles
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Text input
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Pointing on a Picture
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 1: Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Question Styles
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Pointing on a Picture
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Getting Answers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
About Exams and Certificates
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
edX Exams
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Getting Answers
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Welcome!
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
30
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Introduction
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Demo Course Overview
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Introduction: Video and Sequences
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Multiple Choice Questions
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 1: Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Question Styles
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Multiple Choice Questions
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Numerical Input
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 1: Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Question Styles
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Numerical Input
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Connecting a Circuit and a Circuit Diagram
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
3
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 1: Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Lesson 1 - Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Video Presentation Styles
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
CAPA
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 2: Get Interactive
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Labs and Demos
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Code Grader
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Interactive Questions
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 1: Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Lesson 1 - Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Interactive Questions
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Blank HTML Page
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
6
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Introduction
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Demo Course Overview
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Introduction: Video and Sequences
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Discussion Forums
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
5
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 3: Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Lesson 3 - Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Discussion Forums
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Overall Grade
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
7
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
About Exams and Certificates
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
edX Exams
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Overall Grade Performance
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Blank HTML Page
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
3
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 3: Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Lesson 3 - Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Find Your Study Buddy
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Find Your Study Buddy
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
3
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 3: Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Find Your Study Buddy
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Homework - Find Your Study Buddy
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Be Social
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
4
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 3: Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Lesson 3 - Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Be Social
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
EdX Exams
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
4
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
About Exams and Certificates
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
edX Exams
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
EdX Exams
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
When Are Your Exams?
|
||||||
|
</span>
|
||||||
|
<em>
|
||||||
|
2
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="courseware-search-results__breadcrumbs"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Example Week 1: Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Lesson 1 - Getting Started
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
When Are Your Exams?
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="courseware-search-results__item"
|
||||||
|
href="https://www.edx.org"
|
||||||
|
rel="nofollow"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="courseware-search-results__title"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
External Course Link Test
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
|
||||||
|
Object {
|
||||||
|
"filters": Array [
|
||||||
|
Object {
|
||||||
|
"count": 7,
|
||||||
|
"key": "capa",
|
||||||
|
"label": "CAPA",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"count": 2,
|
||||||
|
"key": "sequence",
|
||||||
|
"label": "Sequence",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"count": 9,
|
||||||
|
"key": "text",
|
||||||
|
"label": "Text",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"count": 1,
|
||||||
|
"key": "unknown",
|
||||||
|
"label": "Unknown",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"count": 2,
|
||||||
|
"key": "video",
|
||||||
|
"label": "Video",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"maxScore": 3.4545178,
|
||||||
|
"ms": 5,
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||||
|
"location": Array [
|
||||||
|
"Introduction",
|
||||||
|
"Demo Course Overview",
|
||||||
|
],
|
||||||
|
"score": 3.4545178,
|
||||||
|
"title": "Demo Course Overview",
|
||||||
|
"type": "sequence",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||||
|
"location": Array [
|
||||||
|
"About Exams and Certificates",
|
||||||
|
"edX Exams",
|
||||||
|
"Passing a Course",
|
||||||
|
],
|
||||||
|
"score": 3.4545178,
|
||||||
|
"title": "Passing a Course",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||||
|
"location": Array [
|
||||||
|
"About Exams and Certificates",
|
||||||
|
"edX Exams",
|
||||||
|
"Passing a Course",
|
||||||
|
],
|
||||||
|
"score": 3.4545178,
|
||||||
|
"title": "Passing a Course",
|
||||||
|
"type": "sequence",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 1: Getting Started",
|
||||||
|
"Homework - Question Styles",
|
||||||
|
"Text input",
|
||||||
|
],
|
||||||
|
"score": 1.5874016,
|
||||||
|
"title": "Text Input",
|
||||||
|
"type": "capa",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 1: Getting Started",
|
||||||
|
"Homework - Question Styles",
|
||||||
|
"Pointing on a Picture",
|
||||||
|
],
|
||||||
|
"score": 1.5499392,
|
||||||
|
"title": "Pointing on a Picture",
|
||||||
|
"type": "capa",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||||
|
"location": Array [
|
||||||
|
"About Exams and Certificates",
|
||||||
|
"edX Exams",
|
||||||
|
"Getting Answers",
|
||||||
|
],
|
||||||
|
"score": 1.5003732,
|
||||||
|
"title": "Getting Answers",
|
||||||
|
"type": "capa",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||||
|
"location": Array [
|
||||||
|
"Introduction",
|
||||||
|
"Demo Course Overview",
|
||||||
|
"Introduction: Video and Sequences",
|
||||||
|
],
|
||||||
|
"score": 1.4792063,
|
||||||
|
"title": "Welcome!",
|
||||||
|
"type": "video",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 1: Getting Started",
|
||||||
|
"Homework - Question Styles",
|
||||||
|
"Multiple Choice Questions",
|
||||||
|
],
|
||||||
|
"score": 1.4341705,
|
||||||
|
"title": "Multiple Choice Questions",
|
||||||
|
"type": "capa",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 1: Getting Started",
|
||||||
|
"Homework - Question Styles",
|
||||||
|
"Numerical Input",
|
||||||
|
],
|
||||||
|
"score": 1.2987298,
|
||||||
|
"title": "Numerical Input",
|
||||||
|
"type": "capa",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 1: Getting Started",
|
||||||
|
"Lesson 1 - Getting Started",
|
||||||
|
"Video Presentation Styles",
|
||||||
|
],
|
||||||
|
"score": 1.1870136,
|
||||||
|
"title": "Connecting a Circuit and a Circuit Diagram",
|
||||||
|
"type": "video",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 2: Get Interactive",
|
||||||
|
"Homework - Labs and Demos",
|
||||||
|
"Code Grader",
|
||||||
|
],
|
||||||
|
"score": 1.0107487,
|
||||||
|
"title": "CAPA",
|
||||||
|
"type": "capa",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 1: Getting Started",
|
||||||
|
"Lesson 1 - Getting Started",
|
||||||
|
"Interactive Questions",
|
||||||
|
],
|
||||||
|
"score": 0.96387196,
|
||||||
|
"title": "Interactive Questions",
|
||||||
|
"type": "capa",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||||
|
"location": Array [
|
||||||
|
"Introduction",
|
||||||
|
"Demo Course Overview",
|
||||||
|
"Introduction: Video and Sequences",
|
||||||
|
],
|
||||||
|
"score": 0.8844358,
|
||||||
|
"title": "Blank HTML Page",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 3: Be Social",
|
||||||
|
"Lesson 3 - Be Social",
|
||||||
|
"Discussion Forums",
|
||||||
|
],
|
||||||
|
"score": 0.8803684,
|
||||||
|
"title": "Discussion Forums",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||||
|
"location": Array [
|
||||||
|
"About Exams and Certificates",
|
||||||
|
"edX Exams",
|
||||||
|
"Overall Grade Performance",
|
||||||
|
],
|
||||||
|
"score": 0.87981963,
|
||||||
|
"title": "Overall Grade",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 3: Be Social",
|
||||||
|
"Lesson 3 - Be Social",
|
||||||
|
"Homework - Find Your Study Buddy",
|
||||||
|
],
|
||||||
|
"score": 0.84284115,
|
||||||
|
"title": "Blank HTML Page",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 3: Be Social",
|
||||||
|
"Homework - Find Your Study Buddy",
|
||||||
|
"Homework - Find Your Study Buddy",
|
||||||
|
],
|
||||||
|
"score": 0.84284115,
|
||||||
|
"title": "Find Your Study Buddy",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 3: Be Social",
|
||||||
|
"Lesson 3 - Be Social",
|
||||||
|
"Be Social",
|
||||||
|
],
|
||||||
|
"score": 0.84210813,
|
||||||
|
"title": "Be Social",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||||
|
"location": Array [
|
||||||
|
"About Exams and Certificates",
|
||||||
|
"edX Exams",
|
||||||
|
"EdX Exams",
|
||||||
|
],
|
||||||
|
"score": 0.8306555,
|
||||||
|
"title": "EdX Exams",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||||
|
"location": Array [
|
||||||
|
"Example Week 1: Getting Started",
|
||||||
|
"Lesson 1 - Getting Started",
|
||||||
|
"When Are Your Exams? ",
|
||||||
|
],
|
||||||
|
"score": 0.82610154,
|
||||||
|
"title": "When Are Your Exams? ",
|
||||||
|
"type": "text",
|
||||||
|
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"contentHits": 0,
|
||||||
|
"id": "random-element-id",
|
||||||
|
"location": null,
|
||||||
|
"score": 0.82610154,
|
||||||
|
"title": "External Course Link Test",
|
||||||
|
"type": "unknown",
|
||||||
|
"url": "https://www.edx.org",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"total": 29,
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -5,25 +5,13 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
border-top: 1px solid $light-300;
|
||||||
height: 100%;
|
z-index: 200;
|
||||||
max-width: none;
|
|
||||||
margin: 0;
|
|
||||||
border-top: 1px solid var(--pgn-color-light-300);
|
|
||||||
z-index: var(--pgn-elevation-modal-zindex); // 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;
|
top: 0.5rem;
|
||||||
right: 0;
|
right: 1rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -47,7 +35,7 @@
|
|||||||
|
|
||||||
&__results-summary {
|
&__results-summary {
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
color: var(--pgn-color-gray-500);
|
color: $gray-500;
|
||||||
padding: 1rem 0 .5rem;
|
padding: 1rem 0 .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +50,7 @@
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
&__empty {
|
&__empty {
|
||||||
color: var(--pgn-color-gray-500);
|
color: $gray-500;
|
||||||
padding: 6rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
@@ -76,17 +62,17 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--pgn-color-light-300);
|
background: $light-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
border-top: 1px solid var(--pgn-color-light-300);
|
border-top: 1px solid $light-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
padding: 0.375rem 0 0 0.375rem;
|
padding: 0.375rem 0 0 0.375rem;
|
||||||
color: var(--pgn-color-gray-300);
|
color: $gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
@@ -99,7 +85,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 2.5;
|
line-height: 2.5;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--pgn-color-black);
|
color: $black;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -113,7 +99,7 @@
|
|||||||
font-variant-numeric: lining-nums tabular-nums;
|
font-variant-numeric: lining-nums tabular-nums;
|
||||||
min-width: 1.25rem;
|
min-width: 1.25rem;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
background: var(--pgn-color-light-300);
|
background: $light-300;
|
||||||
border-radius: 99rem;
|
border-radius: 99rem;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
margin-left: 0.375rem;
|
margin-left: 0.375rem;
|
||||||
@@ -125,7 +111,7 @@
|
|||||||
&__breadcrumbs {
|
&__breadcrumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
color: var(--pgn-color-gray-500);
|
color: $gray-500;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -156,24 +142,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.courseware-search-results-tabs {
|
.courseware-search-results-tabs {
|
||||||
border-bottom-color: var(--pgn-color-gray-400) !important;
|
border-bottom-color: $gray-400 !important;
|
||||||
|
|
||||||
&.nav-tabs .nav-link.active {
|
&.nav-tabs .nav-link.active {
|
||||||
border-bottom-width: 4px !important;
|
border-bottom-width: 4px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (--pgn-size-breakpoint-min-width-md) {
|
@media (min-width: map-get($grid-breakpoints, 'md')) {
|
||||||
.courseware-search {
|
.courseware-search__content {
|
||||||
&__close {
|
padding-top: 8rem;
|
||||||
right: -2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
padding-top: 8rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body._search-no-scroll {
|
body._search-no-scroll {
|
||||||
|
|||||||
@@ -70,13 +70,12 @@ export function useLockScroll() {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initSearchParams = { q: '', f: '' };
|
|
||||||
export function useCoursewareSearchParams() {
|
export function useCoursewareSearchParams() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams(initSearchParams);
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const clearSearchParams = () => setSearchParams(initSearchParams);
|
const clearSearchParams = () => setSearchParams({ q: '', f: '' });
|
||||||
|
|
||||||
const query = searchParams.get('q');
|
const query = searchParams.get('q');
|
||||||
const filter = searchParams.get('f')?.toLowerCase();
|
const filter = searchParams.get('f');
|
||||||
|
|
||||||
const setQuery = (q) => setSearchParams((params) => ({ q, f: params.get('f') }));
|
const setQuery = (q) => setSearchParams((params) => ({ q, f: params.get('f') }));
|
||||||
const setFilter = (f) => setSearchParams((params) => ({ q: params.get('q'), f }));
|
const setFilter = (f) => setSearchParams((params) => ({ q: params.get('q'), f }));
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
import { renderHook, act } from '@testing-library/react-hooks';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { fetchCoursewareSearchSettings } from '../data/thunks';
|
import { fetchCoursewareSearchSettings } from '../data/thunks';
|
||||||
import {
|
import {
|
||||||
useCoursewareSearchFeatureFlag,
|
useCoursewareSearchFeatureFlag, useCoursewareSearchState, useElementBoundingBox, useLockScroll,
|
||||||
useCoursewareSearchParams,
|
|
||||||
useCoursewareSearchState,
|
|
||||||
useElementBoundingBox,
|
|
||||||
useLockScroll,
|
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
|
|
||||||
jest.mock('react-redux');
|
jest.mock('react-redux');
|
||||||
@@ -38,13 +34,13 @@ describe('CoursewareSearch Hooks', () => {
|
|||||||
|
|
||||||
it('should return true if feature is enabled', async () => {
|
it('should return true if feature is enabled', async () => {
|
||||||
const hook = await renderTestHook();
|
const hook = await renderTestHook();
|
||||||
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||||
expect(hook.result.current).toBe(true);
|
expect(hook.result.current).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if feature is disabled', async () => {
|
it('should return false if feature is disabled', async () => {
|
||||||
const hook = await renderTestHook(false);
|
const hook = await renderTestHook(false);
|
||||||
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||||
expect(hook.result.current).toBe(false);
|
expect(hook.result.current).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -125,7 +121,7 @@ describe('CoursewareSearch Hooks', () => {
|
|||||||
it('should return the element bounding box', async () => {
|
it('should return the element bounding box', async () => {
|
||||||
const hook = await renderTestHook({ elementId: 'test', mockedInfo });
|
const hook = await renderTestHook({ elementId: 'test', mockedInfo });
|
||||||
|
|
||||||
await waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
hook.waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
||||||
|
|
||||||
expect(hook.result.current).toEqual(mockedInfo);
|
expect(hook.result.current).toEqual(mockedInfo);
|
||||||
});
|
});
|
||||||
@@ -188,45 +184,4 @@ describe('CoursewareSearch Hooks', () => {
|
|||||||
expect(removeBodyClassSpy).toHaveBeenCalledWith('_search-no-scroll');
|
expect(removeBodyClassSpy).toHaveBeenCalledWith('_search-no-scroll');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useSearchParams', () => {
|
|
||||||
const initSearch = { q: '', f: '' };
|
|
||||||
const q = { value: '' };
|
|
||||||
const f = { value: '' };
|
|
||||||
const mockedQuery = { q, f };
|
|
||||||
const searchParams = { get: (prop) => mockedQuery[prop].value };
|
|
||||||
const setSearchParams = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useSearchParams.mockImplementation(() => [searchParams, setSearchParams]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should init the search params properly', () => {
|
|
||||||
const {
|
|
||||||
query, filter, setQuery, setFilter, clearSearchParams,
|
|
||||||
} = useCoursewareSearchParams();
|
|
||||||
|
|
||||||
expect(useSearchParams).toBeCalledWith(initSearch);
|
|
||||||
expect(query).toBe('');
|
|
||||||
expect(filter).toBe('');
|
|
||||||
|
|
||||||
setQuery('setQuery');
|
|
||||||
expect(setSearchParams).toBeCalledWith(expect.any(Function));
|
|
||||||
|
|
||||||
setFilter('setFilter');
|
|
||||||
expect(setSearchParams).toBeCalledWith(expect.any(Function));
|
|
||||||
|
|
||||||
clearSearchParams();
|
|
||||||
expect(setSearchParams).toBeCalledWith(initSearch);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the query and lowercase filter if any', () => {
|
|
||||||
q.value = '42';
|
|
||||||
f.value = 'LOWERCASE';
|
|
||||||
const { query, filter } = useCoursewareSearchParams();
|
|
||||||
|
|
||||||
expect(query).toBe('42');
|
|
||||||
expect(filter).toBe('lowercase');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ describe('mapSearchResponse', () => {
|
|||||||
response = mapSearchResponse(camelCaseObject(mockedResponse));
|
response = mapSearchResponse(camelCaseObject(mockedResponse));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match number of results', () => {
|
it('should match snapshot', () => {
|
||||||
expect(response.results.length).toBe(mockedResponse.results.length);
|
expect(response).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match expected filters', () => {
|
it('should match expected filters', () => {
|
||||||
@@ -24,25 +24,6 @@ describe('mapSearchResponse', () => {
|
|||||||
];
|
];
|
||||||
expect(response.filters).toEqual(expectedFilters);
|
expect(response.filters).toEqual(expectedFilters);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match expected results', () => {
|
|
||||||
const mockFirstResult = mockedResponse.results[0];
|
|
||||||
const expectedFirstResult = {
|
|
||||||
id: mockFirstResult.data.id,
|
|
||||||
title: mockFirstResult.data.content.display_name,
|
|
||||||
type: mockFirstResult.data.content_type.toLowerCase(),
|
|
||||||
location: mockFirstResult.data.location,
|
|
||||||
url: mockFirstResult.data.url,
|
|
||||||
contentHits: 0,
|
|
||||||
score: mockFirstResult.score,
|
|
||||||
};
|
|
||||||
expect(response.results[0]).toEqual(expectedFirstResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should match expected ms and max score', () => {
|
|
||||||
expect(response.maxScore).toBe(mockedResponse.max_score);
|
|
||||||
expect(response.ms).toBe(mockedResponse.took);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the a keyword is provided', () => {
|
describe('when the a keyword is provided', () => {
|
||||||
|
|||||||
@@ -2,84 +2,79 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
searchOpenAction: {
|
searchOpenAction: {
|
||||||
id: 'learn.coursewareSearch.openAction',
|
id: 'learn.coursewareSerch.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.coursewareSearch.submitLabel',
|
id: 'learn.coursewareSerch.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.coursewareSearch.clearAction',
|
id: 'learn.coursewareSerch.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.coursewareSearch.closeAction',
|
id: 'learn.coursewareSerch.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.coursewareSearch.searchModuleTitle',
|
id: 'learn.coursewareSerch.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.coursewareSearch.searchBarPlaceholderText',
|
id: 'learn.coursewareSerch.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.coursewareSearch.loading',
|
id: 'learn.coursewareSerch.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.coursewareSearch.searchResultsNone',
|
id: 'learn.coursewareSerch.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.coursewareSearch.searchResultsLabel',
|
id: 'learn.coursewareSerch.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.coursewareSearch.searchResultsError',
|
id: 'learn.coursewareSerch.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.',
|
||||||
},
|
},
|
||||||
|
|
||||||
// These are translations for labeling the filters
|
// These are translations for labeling the filters
|
||||||
'filter:all': {
|
'filter:all': {
|
||||||
id: 'learn.coursewareSearch.filter:all',
|
id: 'learn.coursewareSerch.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.coursewareSearch.filter:text',
|
id: 'learn.coursewareSerch.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.coursewareSearch.filter:video',
|
id: 'learn.coursewareSerch.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.coursewareSearch.filter:sequence',
|
id: 'learn.coursewareSerch.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.coursewareSearch.filter:other',
|
id: 'learn.coursewareSerch.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,7 +6,6 @@ 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,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ Factory.define('outlineTabData')
|
|||||||
course_access_redirect: false,
|
course_access_redirect: false,
|
||||||
has_scheduled_content: null,
|
has_scheduled_content: null,
|
||||||
access_expiration: null,
|
access_expiration: null,
|
||||||
|
can_show_upgrade_sock: false,
|
||||||
cert_data: {
|
cert_data: {
|
||||||
cert_status: null,
|
cert_status: null,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
|
|||||||
@@ -17,21 +17,7 @@ Factory.define('progressTabData')
|
|||||||
percent: 1,
|
percent: 1,
|
||||||
is_passing: true,
|
is_passing: true,
|
||||||
},
|
},
|
||||||
final_grades: 0.5,
|
|
||||||
credit_course_requirements: null,
|
credit_course_requirements: null,
|
||||||
assignment_type_grade_summary: [
|
|
||||||
{
|
|
||||||
type: 'Homework',
|
|
||||||
short_label: 'HW',
|
|
||||||
weight: 1,
|
|
||||||
average_grade: 1,
|
|
||||||
weighted_grade: 1,
|
|
||||||
num_droppable: 1,
|
|
||||||
num_total: 2,
|
|
||||||
has_hidden_contribution: 'none',
|
|
||||||
last_grade_publish_date: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
section_scores: [
|
section_scores: [
|
||||||
{
|
{
|
||||||
display_name: 'First section',
|
display_name: 'First section',
|
||||||
|
|||||||
729
src/course-home/data/__snapshots__/redux.test.js.snap
Normal file
729
src/course-home/data/__snapshots__/redux.test.js.snap
Normal file
@@ -0,0 +1,729 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
||||||
|
Object {
|
||||||
|
"courseHome": Object {
|
||||||
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"courseStatus": "loaded",
|
||||||
|
"proctoringPanelStatus": "loading",
|
||||||
|
"showSearch": false,
|
||||||
|
"targetUserId": undefined,
|
||||||
|
"toastBodyLink": null,
|
||||||
|
"toastBodyText": null,
|
||||||
|
"toastHeader": "",
|
||||||
|
},
|
||||||
|
"courseware": Object {
|
||||||
|
"courseId": null,
|
||||||
|
"courseStatus": "loading",
|
||||||
|
"sequenceId": null,
|
||||||
|
"sequenceMightBeUnit": false,
|
||||||
|
"sequenceStatus": "loading",
|
||||||
|
},
|
||||||
|
"learningAssistant": ObjectContaining {
|
||||||
|
"conversationId": Any<String>,
|
||||||
|
},
|
||||||
|
"models": Object {
|
||||||
|
"courseHomeMeta": Object {
|
||||||
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
|
"canViewCertificate": true,
|
||||||
|
"celebrations": null,
|
||||||
|
"courseAccess": Object {
|
||||||
|
"additionalContextUserMessage": null,
|
||||||
|
"developerMessage": null,
|
||||||
|
"errorCode": null,
|
||||||
|
"hasAccess": true,
|
||||||
|
"userFragment": null,
|
||||||
|
"userMessage": null,
|
||||||
|
},
|
||||||
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"isEnrolled": false,
|
||||||
|
"isMasquerading": false,
|
||||||
|
"isSelfPaced": false,
|
||||||
|
"isStaff": false,
|
||||||
|
"number": "DemoX",
|
||||||
|
"org": "edX",
|
||||||
|
"originalUserIsStaff": false,
|
||||||
|
"start": "2013-02-05T05:00:00Z",
|
||||||
|
"tabs": Array [
|
||||||
|
Object {
|
||||||
|
"slug": "outline",
|
||||||
|
"title": "Course",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "discussion",
|
||||||
|
"title": "Discussion",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "wiki",
|
||||||
|
"title": "Wiki",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "progress",
|
||||||
|
"title": "Progress",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "instructor",
|
||||||
|
"title": "Instructor",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "dates",
|
||||||
|
"title": "Dates",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Demonstration Course",
|
||||||
|
"userTimezone": "UTC",
|
||||||
|
"username": "MockUser",
|
||||||
|
"verifiedMode": Object {
|
||||||
|
"accessExpirationDate": null,
|
||||||
|
"currency": "USD",
|
||||||
|
"currencySymbol": "$",
|
||||||
|
"price": 149,
|
||||||
|
"sku": "8CF08E5",
|
||||||
|
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dates": Object {
|
||||||
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
|
"courseDateBlocks": Array [
|
||||||
|
Object {
|
||||||
|
"date": "2020-05-01T17:59:41Z",
|
||||||
|
"dateType": "course-start-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "",
|
||||||
|
"title": "Course Starts",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"complete": true,
|
||||||
|
"date": "2020-05-04T02:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"title": "Multi Badges Completed",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2020-05-05T02:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"title": "Multi Badges Past Due",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2020-05-27T02:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Both Past Due 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2020-05-27T02:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Both Past Due 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"complete": true,
|
||||||
|
"date": "2020-05-28T08:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "One Completed/Due 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2020-05-28T08:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "One Completed/Due 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"complete": true,
|
||||||
|
"date": "2020-05-29T08:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Both Completed 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"complete": true,
|
||||||
|
"date": "2020-05-29T08:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Both Completed 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"date": "2020-06-16T17:59:40.942669Z",
|
||||||
|
"dateType": "verified-upgrade-deadline",
|
||||||
|
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Upgrade to Verified Certificate",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-17T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": false,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "One Verified 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-17T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "One Verified 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-17T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": "ORA Dates are set by the instructor, and can't be changed",
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "ORA Verified 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-18T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": false,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Both Verified 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-18T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": false,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Both Verified 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-19T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"title": "One Unreleased 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-19T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "One Unreleased 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-20T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"title": "Both Unreleased 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"date": "2030-08-20T05:59:40.942669Z",
|
||||||
|
"dateType": "assignment-due-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"title": "Both Unreleased 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"date": "2030-08-23T00:00:00Z",
|
||||||
|
"dateType": "course-end-date",
|
||||||
|
"description": "",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"link": "",
|
||||||
|
"title": "Course Ends",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"date": "2030-09-01T00:00:00Z",
|
||||||
|
"dateType": "verification-deadline-date",
|
||||||
|
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
|
||||||
|
"extraInfo": null,
|
||||||
|
"learnerHasAccess": false,
|
||||||
|
"link": "https://example.com/",
|
||||||
|
"title": "Verification Deadline",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"datesBannerInfo": Object {
|
||||||
|
"contentTypeGatingEnabled": false,
|
||||||
|
"missedDeadlines": false,
|
||||||
|
"missedGatedContent": false,
|
||||||
|
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
|
},
|
||||||
|
"hasEnded": false,
|
||||||
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"learnerIsFullAccess": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"recommendations": Object {
|
||||||
|
"recommendationsStatus": "loading",
|
||||||
|
},
|
||||||
|
"tours": Object {
|
||||||
|
"showCoursewareTour": false,
|
||||||
|
"showExistingUserCourseHomeTour": false,
|
||||||
|
"showNewUserCourseHomeModal": false,
|
||||||
|
"showNewUserCourseHomeTour": false,
|
||||||
|
"toursEnabled": false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
||||||
|
Object {
|
||||||
|
"courseHome": Object {
|
||||||
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"courseStatus": "loaded",
|
||||||
|
"proctoringPanelStatus": "loading",
|
||||||
|
"showSearch": false,
|
||||||
|
"targetUserId": undefined,
|
||||||
|
"toastBodyLink": null,
|
||||||
|
"toastBodyText": null,
|
||||||
|
"toastHeader": "",
|
||||||
|
},
|
||||||
|
"courseware": Object {
|
||||||
|
"courseId": null,
|
||||||
|
"courseStatus": "loading",
|
||||||
|
"sequenceId": null,
|
||||||
|
"sequenceMightBeUnit": false,
|
||||||
|
"sequenceStatus": "loading",
|
||||||
|
},
|
||||||
|
"learningAssistant": ObjectContaining {
|
||||||
|
"conversationId": Any<String>,
|
||||||
|
},
|
||||||
|
"models": Object {
|
||||||
|
"courseHomeMeta": Object {
|
||||||
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
|
"canViewCertificate": true,
|
||||||
|
"celebrations": null,
|
||||||
|
"courseAccess": Object {
|
||||||
|
"additionalContextUserMessage": null,
|
||||||
|
"developerMessage": null,
|
||||||
|
"errorCode": null,
|
||||||
|
"hasAccess": true,
|
||||||
|
"userFragment": null,
|
||||||
|
"userMessage": null,
|
||||||
|
},
|
||||||
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"isEnrolled": false,
|
||||||
|
"isMasquerading": false,
|
||||||
|
"isSelfPaced": false,
|
||||||
|
"isStaff": false,
|
||||||
|
"number": "DemoX",
|
||||||
|
"org": "edX",
|
||||||
|
"originalUserIsStaff": false,
|
||||||
|
"start": "2013-02-05T05:00:00Z",
|
||||||
|
"tabs": Array [
|
||||||
|
Object {
|
||||||
|
"slug": "outline",
|
||||||
|
"title": "Course",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "discussion",
|
||||||
|
"title": "Discussion",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "wiki",
|
||||||
|
"title": "Wiki",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "progress",
|
||||||
|
"title": "Progress",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "instructor",
|
||||||
|
"title": "Instructor",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "dates",
|
||||||
|
"title": "Dates",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Demonstration Course",
|
||||||
|
"userTimezone": "UTC",
|
||||||
|
"username": "MockUser",
|
||||||
|
"verifiedMode": Object {
|
||||||
|
"accessExpirationDate": null,
|
||||||
|
"currency": "USD",
|
||||||
|
"currencySymbol": "$",
|
||||||
|
"price": 149,
|
||||||
|
"sku": "8CF08E5",
|
||||||
|
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"outline": Object {
|
||||||
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
|
"accessExpiration": null,
|
||||||
|
"canShowUpgradeSock": false,
|
||||||
|
"certData": Object {
|
||||||
|
"certStatus": null,
|
||||||
|
"certWebViewUrl": null,
|
||||||
|
"certificateAvailableDate": null,
|
||||||
|
},
|
||||||
|
"courseBlocks": Object {
|
||||||
|
"courses": Object {
|
||||||
|
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
|
||||||
|
"hasScheduledContent": false,
|
||||||
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"sectionIds": Array [
|
||||||
|
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
|
],
|
||||||
|
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sections": Object {
|
||||||
|
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
|
||||||
|
"complete": false,
|
||||||
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
|
"resumeBlock": false,
|
||||||
|
"sequenceIds": Array [
|
||||||
|
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||||
|
],
|
||||||
|
"title": "Title of Section",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sequences": Object {
|
||||||
|
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object {
|
||||||
|
"complete": false,
|
||||||
|
"description": null,
|
||||||
|
"due": null,
|
||||||
|
"effortActivities": 2,
|
||||||
|
"effortTime": 15,
|
||||||
|
"icon": null,
|
||||||
|
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||||
|
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
|
"showLink": true,
|
||||||
|
"title": "Title of Sequence",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"courseGoals": Object {
|
||||||
|
"daysPerWeek": null,
|
||||||
|
"goalOptions": Array [],
|
||||||
|
"selectedGoal": null,
|
||||||
|
"subscribedToReminders": null,
|
||||||
|
"weeklyLearningGoalEnabled": false,
|
||||||
|
},
|
||||||
|
"courseTools": Array [
|
||||||
|
Object {
|
||||||
|
"analyticsId": "edx.bookmarks",
|
||||||
|
"title": "Bookmarks",
|
||||||
|
"url": "https://example.com/bookmarks",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"datesBannerInfo": Object {
|
||||||
|
"contentTypeGatingEnabled": false,
|
||||||
|
"missedDeadlines": false,
|
||||||
|
"missedGatedContent": false,
|
||||||
|
},
|
||||||
|
"datesWidget": Object {
|
||||||
|
"courseDateBlocks": Array [],
|
||||||
|
},
|
||||||
|
"enableProctoredExams": undefined,
|
||||||
|
"enrollAlert": Object {
|
||||||
|
"canEnroll": true,
|
||||||
|
"extraText": "Contact the administrator.",
|
||||||
|
},
|
||||||
|
"enrollmentMode": undefined,
|
||||||
|
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
||||||
|
"hasEnded": undefined,
|
||||||
|
"hasScheduledContent": null,
|
||||||
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"offer": null,
|
||||||
|
"resumeCourse": Object {
|
||||||
|
"hasVisitedCourse": false,
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||||
|
},
|
||||||
|
"timeOffsetMillis": 0,
|
||||||
|
"userHasPassingGrade": undefined,
|
||||||
|
"verifiedMode": Object {
|
||||||
|
"accessExpirationDate": "2050-01-01T12:00:00",
|
||||||
|
"currency": "USD",
|
||||||
|
"currencySymbol": "$",
|
||||||
|
"price": 149,
|
||||||
|
"sku": "ABCD1234",
|
||||||
|
"upgradeUrl": "http://localhost:18000/dashboard",
|
||||||
|
},
|
||||||
|
"welcomeMessageHtml": "<p>Welcome to this course!</p>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"recommendations": Object {
|
||||||
|
"recommendationsStatus": "loading",
|
||||||
|
},
|
||||||
|
"tours": Object {
|
||||||
|
"showCoursewareTour": false,
|
||||||
|
"showExistingUserCourseHomeTour": false,
|
||||||
|
"showNewUserCourseHomeModal": false,
|
||||||
|
"showNewUserCourseHomeTour": false,
|
||||||
|
"toursEnabled": false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
||||||
|
Object {
|
||||||
|
"courseHome": Object {
|
||||||
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"courseStatus": "loaded",
|
||||||
|
"proctoringPanelStatus": "loading",
|
||||||
|
"showSearch": false,
|
||||||
|
"targetUserId": undefined,
|
||||||
|
"toastBodyLink": null,
|
||||||
|
"toastBodyText": null,
|
||||||
|
"toastHeader": "",
|
||||||
|
},
|
||||||
|
"courseware": Object {
|
||||||
|
"courseId": null,
|
||||||
|
"courseStatus": "loading",
|
||||||
|
"sequenceId": null,
|
||||||
|
"sequenceMightBeUnit": false,
|
||||||
|
"sequenceStatus": "loading",
|
||||||
|
},
|
||||||
|
"learningAssistant": ObjectContaining {
|
||||||
|
"conversationId": Any<String>,
|
||||||
|
},
|
||||||
|
"models": Object {
|
||||||
|
"courseHomeMeta": Object {
|
||||||
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
|
"canViewCertificate": true,
|
||||||
|
"celebrations": null,
|
||||||
|
"courseAccess": Object {
|
||||||
|
"additionalContextUserMessage": null,
|
||||||
|
"developerMessage": null,
|
||||||
|
"errorCode": null,
|
||||||
|
"hasAccess": true,
|
||||||
|
"userFragment": null,
|
||||||
|
"userMessage": null,
|
||||||
|
},
|
||||||
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"isEnrolled": false,
|
||||||
|
"isMasquerading": false,
|
||||||
|
"isSelfPaced": false,
|
||||||
|
"isStaff": false,
|
||||||
|
"number": "DemoX",
|
||||||
|
"org": "edX",
|
||||||
|
"originalUserIsStaff": false,
|
||||||
|
"start": "2013-02-05T05:00:00Z",
|
||||||
|
"tabs": Array [
|
||||||
|
Object {
|
||||||
|
"slug": "outline",
|
||||||
|
"title": "Course",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "discussion",
|
||||||
|
"title": "Discussion",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "wiki",
|
||||||
|
"title": "Wiki",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "progress",
|
||||||
|
"title": "Progress",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "instructor",
|
||||||
|
"title": "Instructor",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"slug": "dates",
|
||||||
|
"title": "Dates",
|
||||||
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Demonstration Course",
|
||||||
|
"userTimezone": "UTC",
|
||||||
|
"username": "MockUser",
|
||||||
|
"verifiedMode": Object {
|
||||||
|
"accessExpirationDate": null,
|
||||||
|
"currency": "USD",
|
||||||
|
"currencySymbol": "$",
|
||||||
|
"price": 149,
|
||||||
|
"sku": "8CF08E5",
|
||||||
|
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"progress": Object {
|
||||||
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
|
"accessExpiration": null,
|
||||||
|
"certificateData": Object {},
|
||||||
|
"completionSummary": Object {
|
||||||
|
"completeCount": 1,
|
||||||
|
"incompleteCount": 1,
|
||||||
|
"lockedCount": 0,
|
||||||
|
},
|
||||||
|
"courseGrade": Object {
|
||||||
|
"isPassing": true,
|
||||||
|
"letterGrade": "pass",
|
||||||
|
"percent": 1,
|
||||||
|
},
|
||||||
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"creditCourseRequirements": null,
|
||||||
|
"end": "3027-03-31T00:00:00Z",
|
||||||
|
"enrollmentMode": "audit",
|
||||||
|
"gradesFeatureIsFullyLocked": false,
|
||||||
|
"gradesFeatureIsPartiallyLocked": false,
|
||||||
|
"gradingPolicy": Object {
|
||||||
|
"assignmentPolicies": Array [
|
||||||
|
Object {
|
||||||
|
"averageGrade": "1.00",
|
||||||
|
"numDroppable": 1,
|
||||||
|
"shortLabel": "HW",
|
||||||
|
"type": "Homework",
|
||||||
|
"weight": 1,
|
||||||
|
"weightedGrade": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"gradeRange": Object {
|
||||||
|
"pass": 0.75,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hasScheduledContent": false,
|
||||||
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"sectionScores": Array [
|
||||||
|
Object {
|
||||||
|
"displayName": "First section",
|
||||||
|
"subsections": Array [
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
|
||||||
|
"displayName": "First subsection",
|
||||||
|
"hasGradedAssignment": true,
|
||||||
|
"learnerHasAccess": true,
|
||||||
|
"numPointsEarned": 0,
|
||||||
|
"numPointsPossible": 3,
|
||||||
|
"percentGraded": 0,
|
||||||
|
"problemScores": Array [
|
||||||
|
Object {
|
||||||
|
"earned": 0,
|
||||||
|
"possible": 1,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"earned": 0,
|
||||||
|
"possible": 1,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"earned": 0,
|
||||||
|
"possible": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"showCorrectness": "always",
|
||||||
|
"showGrades": true,
|
||||||
|
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"displayName": "Second section",
|
||||||
|
"subsections": Array [
|
||||||
|
Object {
|
||||||
|
"assignmentType": "Homework",
|
||||||
|
"displayName": "Second subsection",
|
||||||
|
"hasGradedAssignment": true,
|
||||||
|
"numPointsEarned": 1,
|
||||||
|
"numPointsPossible": 1,
|
||||||
|
"percentGraded": 1,
|
||||||
|
"problemScores": Array [
|
||||||
|
Object {
|
||||||
|
"earned": 1,
|
||||||
|
"possible": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"showCorrectness": "always",
|
||||||
|
"showGrades": true,
|
||||||
|
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
|
||||||
|
"userHasPassingGrade": false,
|
||||||
|
"verificationData": Object {
|
||||||
|
"link": null,
|
||||||
|
"status": "none",
|
||||||
|
"statusDate": null,
|
||||||
|
},
|
||||||
|
"verifiedMode": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"recommendations": Object {
|
||||||
|
"recommendationsStatus": "loading",
|
||||||
|
},
|
||||||
|
"tours": Object {
|
||||||
|
"showCoursewareTour": false,
|
||||||
|
"showExistingUserCourseHomeTour": false,
|
||||||
|
"showNewUserCourseHomeModal": false,
|
||||||
|
"showNewUserCourseHomeTour": false,
|
||||||
|
"toursEnabled": false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -3,6 +3,93 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|||||||
import { logInfo } from '@edx/frontend-platform/logging';
|
import { logInfo } from '@edx/frontend-platform/logging';
|
||||||
import { appendBrowserTimezoneToUrl } from '../../utils';
|
import { appendBrowserTimezoneToUrl } from '../../utils';
|
||||||
|
|
||||||
|
const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => {
|
||||||
|
let dropCount = numDroppable;
|
||||||
|
// Drop the lowest grades
|
||||||
|
while (dropCount && points.length >= dropCount) {
|
||||||
|
const lowestScore = Math.min(...points);
|
||||||
|
const lowestScoreIndex = points.indexOf(lowestScore);
|
||||||
|
points.splice(lowestScoreIndex, 1);
|
||||||
|
dropCount--;
|
||||||
|
}
|
||||||
|
let averageGrade = 0;
|
||||||
|
let weightedGrade = 0;
|
||||||
|
if (points.length) {
|
||||||
|
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
|
||||||
|
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
|
||||||
|
// exists in edx-platform.
|
||||||
|
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(2);
|
||||||
|
weightedGrade = averageGrade * assignmentWeight;
|
||||||
|
}
|
||||||
|
return { averageGrade, weightedGrade };
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
|
||||||
|
const gradeByAssignmentType = {};
|
||||||
|
assignmentPolicies.forEach(assignment => {
|
||||||
|
// Create an array with the number of total assignments and set the scores to 0
|
||||||
|
// as placeholders for assignments that have not yet been released
|
||||||
|
gradeByAssignmentType[assignment.type] = {
|
||||||
|
grades: Array(assignment.numTotal).fill(0),
|
||||||
|
numAssignmentsCreated: 0,
|
||||||
|
numTotalExpectedAssignments: assignment.numTotal,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
sectionScores.forEach((chapter) => {
|
||||||
|
chapter.subsections.forEach((subsection) => {
|
||||||
|
if (!(subsection.hasGradedAssignment && subsection.showGrades && subsection.numPointsPossible)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
assignmentType,
|
||||||
|
numPointsEarned,
|
||||||
|
numPointsPossible,
|
||||||
|
} = subsection;
|
||||||
|
|
||||||
|
// If a subsection's assignment type does not match an assignment policy in Studio,
|
||||||
|
// we won't be able to include it in this accumulation of grades by assignment type.
|
||||||
|
// This may happen if a course author has removed/renamed an assignment policy in Studio and
|
||||||
|
// neglected to update the subsection's of that assignment type
|
||||||
|
if (!gradeByAssignmentType[assignmentType]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
numAssignmentsCreated,
|
||||||
|
} = gradeByAssignmentType[assignmentType];
|
||||||
|
|
||||||
|
numAssignmentsCreated++;
|
||||||
|
if (numAssignmentsCreated <= gradeByAssignmentType[assignmentType].numTotalExpectedAssignments) {
|
||||||
|
// Remove a placeholder grade so long as the number of recorded created assignments is less than the number
|
||||||
|
// of expected assignments
|
||||||
|
gradeByAssignmentType[assignmentType].grades.shift();
|
||||||
|
}
|
||||||
|
// Add the graded assignment to the list
|
||||||
|
gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0);
|
||||||
|
// Record the created assignment
|
||||||
|
gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return assignmentPolicies.map((assignment) => {
|
||||||
|
const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades(
|
||||||
|
gradeByAssignmentType[assignment.type].grades,
|
||||||
|
assignment.weight,
|
||||||
|
assignment.numDroppable,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
averageGrade,
|
||||||
|
numDroppable: assignment.numDroppable,
|
||||||
|
shortLabel: assignment.shortLabel,
|
||||||
|
type: assignment.type,
|
||||||
|
weight: assignment.weight,
|
||||||
|
weightedGrade,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tweak the metadata for consistency
|
* Tweak the metadata for consistency
|
||||||
* @param metadata the data to normalize
|
* @param metadata the data to normalize
|
||||||
@@ -49,7 +136,6 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
|||||||
title: block.display_name,
|
title: block.display_name,
|
||||||
resumeBlock: block.resume_block,
|
resumeBlock: block.resume_block,
|
||||||
sequenceIds: block.children || [],
|
sequenceIds: block.children || [],
|
||||||
hideFromTOC: block.hide_from_toc,
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -66,8 +152,6 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
|||||||
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
|
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
|
||||||
showLink: !!block.lms_web_url,
|
showLink: !!block.lms_web_url,
|
||||||
title: block.display_name,
|
title: block.display_name,
|
||||||
hideFromTOC: block.hide_from_toc,
|
|
||||||
navigationDisabled: block.navigation_disabled,
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -149,6 +233,11 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
const camelCasedData = camelCaseObject(data);
|
const camelCasedData = camelCaseObject(data);
|
||||||
|
|
||||||
|
camelCasedData.gradingPolicy.assignmentPolicies = normalizeAssignmentPolicies(
|
||||||
|
camelCasedData.gradingPolicy.assignmentPolicies,
|
||||||
|
camelCasedData.sectionScores,
|
||||||
|
);
|
||||||
|
|
||||||
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
||||||
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
||||||
// in order to preserve a course team's desired grade formatting.
|
// in order to preserve a course team's desired grade formatting.
|
||||||
@@ -197,17 +286,9 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getProctoringInfoData(courseId, username) {
|
export async function getProctoringInfoData(courseId, username) {
|
||||||
let url;
|
let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`;
|
||||||
if (!getConfig().EXAMS_BASE_URL) {
|
if (username) {
|
||||||
url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`;
|
url += `&username=${encodeURIComponent(username)}`;
|
||||||
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);
|
||||||
@@ -275,6 +356,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
} = tabData;
|
} = tabData;
|
||||||
|
|
||||||
const accessExpiration = camelCaseObject(data.access_expiration);
|
const accessExpiration = camelCaseObject(data.access_expiration);
|
||||||
|
const canShowUpgradeSock = data.can_show_upgrade_sock;
|
||||||
const certData = camelCaseObject(data.cert_data);
|
const certData = camelCaseObject(data.cert_data);
|
||||||
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
|
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
|
||||||
const courseGoals = camelCaseObject(data.course_goals);
|
const courseGoals = camelCaseObject(data.course_goals);
|
||||||
@@ -296,6 +378,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
|
canShowUpgradeSock,
|
||||||
certData,
|
certData,
|
||||||
courseBlocks,
|
courseBlocks,
|
||||||
courseGoals,
|
courseGoals,
|
||||||
@@ -363,7 +446,7 @@ export async function unsubscribeFromCourseGoal(token) {
|
|||||||
.then(res => camelCaseObject(res));
|
.then(res => camelCaseObject(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCoursewareSearchEnabled(courseId) {
|
export async function getCoursewareSearchEnabledFlag(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 };
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ describe('Course Home Service', () => {
|
|||||||
willRespondWith: {
|
willRespondWith: {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
|
can_show_upgrade_sock: boolean(false),
|
||||||
verified_mode: like({
|
verified_mode: like({
|
||||||
access_expiration_date: null,
|
access_expiration_date: null,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
@@ -88,11 +89,11 @@ describe('Course Home Service', () => {
|
|||||||
}),
|
}),
|
||||||
title: string('Demonstration Course'),
|
title: string('Demonstration Course'),
|
||||||
username: string('edx'),
|
username: string('edx'),
|
||||||
has_course_author_access: boolean(true),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const normalizedTabData = {
|
const normalizedTabData = {
|
||||||
|
canShowUpgradeSock: false,
|
||||||
verifiedMode: {
|
verifiedMode: {
|
||||||
accessExpirationDate: null,
|
accessExpirationDate: null,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
@@ -132,7 +133,6 @@ 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();
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ describe('Data layer integration tests', () => {
|
|||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toEqual(expect.objectContaining({
|
expect(state).toMatchSnapshot({
|
||||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||||
// to keep track of conversations. This UUID is generated on each run.
|
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||||
// Instead, we use an asymmetric matcher here.
|
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||||
learningAssistant: expect.objectContaining({
|
learningAssistant: expect.objectContaining({
|
||||||
conversationId: expect.any(String),
|
conversationId: expect.any(String),
|
||||||
}),
|
}),
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([401, 403, 404])(
|
it.each([401, 403, 404])(
|
||||||
@@ -137,14 +137,14 @@ describe('Data layer integration tests', () => {
|
|||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toEqual(expect.objectContaining({
|
expect(state).toMatchSnapshot({
|
||||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||||
// to keep track of conversations. This UUID is generated on each run.
|
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||||
// Instead, we use an asymmetric matcher here.
|
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||||
learningAssistant: expect.objectContaining({
|
learningAssistant: expect.objectContaining({
|
||||||
conversationId: expect.any(String),
|
conversationId: expect.any(String),
|
||||||
}),
|
}),
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([401, 403, 404])(
|
it.each([401, 403, 404])(
|
||||||
@@ -185,14 +185,14 @@ describe('Data layer integration tests', () => {
|
|||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toEqual(expect.objectContaining({
|
expect(state).toMatchSnapshot({
|
||||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||||
// to keep track of conversations. This UUID is generated on each run.
|
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||||
// Instead, we use an asymmetric matcher here.
|
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||||
learningAssistant: expect.objectContaining({
|
learningAssistant: expect.objectContaining({
|
||||||
conversationId: expect.any(String),
|
conversationId: expect.any(String),
|
||||||
}),
|
}),
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should handle the url including a targetUserId', async () => {
|
it('Should handle the url including a targetUserId', async () => {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import {
|
export const LOADING = 'loading';
|
||||||
LOADING,
|
export const LOADED = 'loaded';
|
||||||
LOADED,
|
export const FAILED = 'failed';
|
||||||
FAILED,
|
export const DENIED = 'denied';
|
||||||
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,
|
||||||
getCoursewareSearchEnabled,
|
getCoursewareSearchEnabledFlag,
|
||||||
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 getCoursewareSearchEnabled(courseId);
|
const { enabled } = await getCoursewareSearchEnabledFlag(courseId);
|
||||||
return { enabled };
|
return { enabled };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { enabled: false };
|
return { enabled: false };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
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 { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import Timeline from './timeline/Timeline';
|
import Timeline from './timeline/Timeline';
|
||||||
@@ -14,8 +14,7 @@ import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
|||||||
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
|
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
|
||||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||||
|
|
||||||
const DatesTab = () => {
|
const DatesTab = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -60,4 +59,8 @@ const DatesTab = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DatesTab;
|
DatesTab.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(DatesTab);
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ 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);
|
||||||
|
|
||||||
@@ -143,12 +142,10 @@ 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
|
||||||
await user.hover(tipIcon);
|
userEvent.hover(tipIcon);
|
||||||
screen.getByText(tipText); // now it's there
|
const tooltip = screen.getByText(tipText); // now it's there
|
||||||
await user.unhover(tipIcon);
|
userEvent.unhover(tipIcon);
|
||||||
await waitFor(() => {
|
await waitForElementToBeRemoved(tooltip); // and it's gone again
|
||||||
expect(screen.queryByText(tipText)).toBeNull(); // and it's gone again
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { useSelector } from 'react-redux';
|
|||||||
import {
|
import {
|
||||||
FormattedDate,
|
FormattedDate,
|
||||||
FormattedTime,
|
FormattedTime,
|
||||||
useIntl,
|
injectIntl,
|
||||||
|
intlShape,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
|
import { Tooltip, OverlayTrigger } from '@edx/paragon';
|
||||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
@@ -19,10 +20,10 @@ import { isLearnerAssignment } from '../utils';
|
|||||||
const Day = ({
|
const Day = ({
|
||||||
date,
|
date,
|
||||||
first,
|
first,
|
||||||
|
intl,
|
||||||
items,
|
items,
|
||||||
last,
|
last,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -107,6 +108,7 @@ const Day = ({
|
|||||||
Day.propTypes = {
|
Day.propTypes = {
|
||||||
date: PropTypes.objectOf(Date).isRequired,
|
date: PropTypes.objectOf(Date).isRequired,
|
||||||
first: PropTypes.bool,
|
first: PropTypes.bool,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
date: PropTypes.string,
|
date: PropTypes.string,
|
||||||
dateType: PropTypes.string,
|
dateType: PropTypes.string,
|
||||||
@@ -124,4 +126,4 @@ Day.defaultProps = {
|
|||||||
last: false,
|
last: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Day;
|
export default injectIntl(Day);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Badge } from '@openedx/paragon';
|
import { Badge } from '@edx/paragon';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { daycmp, isLearnerAssignment } from '../utils';
|
import { daycmp, isLearnerAssignment } from '../utils';
|
||||||
@@ -38,21 +38,21 @@ function getBadgeListAndColor(date, intl, item, items) {
|
|||||||
message: messages.today,
|
message: messages.today,
|
||||||
shownForDay: isToday,
|
shownForDay: isToday,
|
||||||
bg: 'bg-warning-300',
|
bg: 'bg-warning-300',
|
||||||
className: 'text-dark',
|
className: 'text-black',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: messages.completed,
|
message: messages.completed,
|
||||||
shownForDay: assignments.length && assignments.every(isComplete),
|
shownForDay: assignments.length && assignments.every(isComplete),
|
||||||
shownForItem: x => isLearnerAssignment(x) && isComplete(x),
|
shownForItem: x => isLearnerAssignment(x) && isComplete(x),
|
||||||
bg: 'bg-light-500',
|
bg: 'bg-light-500',
|
||||||
className: 'text-dark',
|
className: 'text-black',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: messages.pastDue,
|
message: messages.pastDue,
|
||||||
shownForDay: assignments.length && assignments.every(isPastDue),
|
shownForDay: assignments.length && assignments.every(isPastDue),
|
||||||
shownForItem: x => isLearnerAssignment(x) && isPastDue(x),
|
shownForItem: x => isLearnerAssignment(x) && isPastDue(x),
|
||||||
bg: 'bg-dark-200',
|
bg: 'bg-dark-200',
|
||||||
className: 'text-dark',
|
className: 'text-white',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: messages.dueNext,
|
message: messages.dueNext,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useParams, generatePath, useNavigate } from 'react-router-dom';
|
import { useParams, generatePath, useNavigate } from 'react-router-dom';
|
||||||
@@ -29,4 +30,6 @@ const DiscussionTab = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DiscussionTab;
|
DiscussionTab.propTypes = {};
|
||||||
|
|
||||||
|
export default injectIntl(DiscussionTab);
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import HeaderSlot from '../../plugin-slots/HeaderSlot';
|
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||||
import PageLoading from '../../generic/PageLoading';
|
import PageLoading from '../../generic/PageLoading';
|
||||||
import { unsubscribeFromCourseGoal } from '../data/api';
|
import { unsubscribeFromCourseGoal } from '../data/api';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ResultPage from './ResultPage';
|
import ResultPage from './ResultPage';
|
||||||
|
|
||||||
const GoalUnsubscribe = () => {
|
const GoalUnsubscribe = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const { token } = useParams();
|
const { token } = useParams();
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -39,7 +38,7 @@ const GoalUnsubscribe = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderSlot showUserDropdown={false} />
|
<Header 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)}`} />
|
||||||
@@ -52,4 +51,8 @@ const GoalUnsubscribe = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GoalUnsubscribe;
|
GoalUnsubscribe.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(GoalUnsubscribe);
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, Hyperlink } from '@openedx/paragon';
|
import { Button, Hyperlink } from '@edx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
|
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
|
||||||
|
|
||||||
const ResultPage = ({ courseTitle, error }) => {
|
const ResultPage = ({ courseTitle, error, intl }) => {
|
||||||
const intl = useIntl();
|
const errorDescription = (
|
||||||
const errorDescription = intl.formatMessage(
|
<FormattedMessage
|
||||||
messages.errorDescription,
|
id="learning.goals.unsubscribe.errorDescription"
|
||||||
{
|
defaultMessage="We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help."
|
||||||
contactSupport: (
|
values={{
|
||||||
<Hyperlink
|
contactSupport: (
|
||||||
className="text-reset"
|
<Hyperlink
|
||||||
style={{ textDecoration: 'underline' }}
|
className="text-reset"
|
||||||
destination={`${getConfig().CONTACT_URL}`}
|
style={{ textDecoration: 'underline' }}
|
||||||
>
|
destination={`${getConfig().CONTACT_URL}`}
|
||||||
{intl.formatMessage(messages.contactSupport)}
|
>
|
||||||
</Hyperlink>
|
{intl.formatMessage(messages.contactSupport)}
|
||||||
),
|
</Hyperlink>
|
||||||
},
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const header = error
|
const header = error
|
||||||
@@ -52,6 +54,7 @@ ResultPage.defaultProps = {
|
|||||||
ResultPage.propTypes = {
|
ResultPage.propTypes = {
|
||||||
courseTitle: PropTypes.string,
|
courseTitle: PropTypes.string,
|
||||||
error: PropTypes.bool,
|
error: PropTypes.bool,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ResultPage;
|
export default injectIntl(ResultPage);
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Something went wrong',
|
defaultMessage: 'Something went wrong',
|
||||||
description: 'It indicate that the unsubscribing request has failed',
|
description: 'It indicate that the unsubscribing request has failed',
|
||||||
},
|
},
|
||||||
errorDescription: {
|
|
||||||
id: 'learning.goals.unsubscribe.errorDescription',
|
|
||||||
defaultMessage: 'We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.',
|
|
||||||
description: 'Message that notifies user that unsubscribing failed and to try again',
|
|
||||||
},
|
|
||||||
goToDashboard: {
|
goToDashboard: {
|
||||||
id: 'learning.goals.unsubscribe.goToDashboard',
|
id: 'learning.goals.unsubscribe.goToDashboard',
|
||||||
defaultMessage: 'Go to dashboard',
|
defaultMessage: 'Go to dashboard',
|
||||||
@@ -65,7 +65,6 @@ const DateSummary = ({
|
|||||||
)}
|
)}
|
||||||
{!linkedTitle && dateBlock.link && (
|
{!linkedTitle && dateBlock.link && (
|
||||||
<a
|
<a
|
||||||
id={dateBlock.dateType === 'verified-upgrade-deadline' ? 'date-verified-upgrade-deadline' : ''}
|
|
||||||
href={dateBlock.link}
|
href={dateBlock.link}
|
||||||
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
||||||
className="description-link"
|
className="description-link"
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ const LmsHtmlFragment = ({
|
|||||||
title,
|
title,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const direction = document.documentElement?.getAttribute('dir') || 'ltr';
|
|
||||||
const wholePage = `
|
const wholePage = `
|
||||||
<html dir="${direction}">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
||||||
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
|
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
|
||||||
@@ -30,7 +29,7 @@ const LmsHtmlFragment = ({
|
|||||||
const iframe = useRef(null);
|
const iframe = useRef(null);
|
||||||
function resetIframeHeight() {
|
function resetIframeHeight() {
|
||||||
if (iframe?.current?.contentWindow?.document?.body) {
|
if (iframe?.current?.contentWindow?.document?.body) {
|
||||||
iframe.current.height = iframe.current.contentWindow.document.body.parentNode.scrollHeight;
|
iframe.current.height = iframe.current.contentWindow.document.body.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, 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 { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@openedx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
import { CourseOutlineTabNotificationsSlot } from '../../plugin-slots/CourseOutlineTabNotificationsSlot';
|
|
||||||
import { AlertList } from '../../generic/user-messages';
|
import { AlertList } from '../../generic/user-messages';
|
||||||
|
|
||||||
import CourseDates from './widgets/CourseDates';
|
import CourseDates from './widgets/CourseDates';
|
||||||
@@ -15,7 +14,9 @@ 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 UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||||
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
||||||
import useCourseEndAlert from './alerts/course-end-alert';
|
import useCourseEndAlert from './alerts/course-end-alert';
|
||||||
@@ -26,10 +27,8 @@ 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 = () => {
|
const OutlineTab = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
proctoringPanelStatus,
|
proctoringPanelStatus,
|
||||||
@@ -39,11 +38,11 @@ const OutlineTab = () => {
|
|||||||
isSelfPaced,
|
isSelfPaced,
|
||||||
org,
|
org,
|
||||||
title,
|
title,
|
||||||
|
userTimezone,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
const expandButtonRef = useRef();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
accessExpiration,
|
||||||
courseBlocks: {
|
courseBlocks: {
|
||||||
courses,
|
courses,
|
||||||
sections,
|
sections,
|
||||||
@@ -52,12 +51,20 @@ const OutlineTab = () => {
|
|||||||
selectedGoal,
|
selectedGoal,
|
||||||
weeklyLearningGoalEnabled,
|
weeklyLearningGoalEnabled,
|
||||||
} = {},
|
} = {},
|
||||||
|
datesBannerInfo,
|
||||||
datesWidget: {
|
datesWidget: {
|
||||||
courseDateBlocks,
|
courseDateBlocks,
|
||||||
},
|
},
|
||||||
enableProctoredExams,
|
enableProctoredExams,
|
||||||
|
offer,
|
||||||
|
timeOffsetMillis,
|
||||||
|
verifiedMode,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
const {
|
||||||
|
marketingUrl,
|
||||||
|
} = useModel('coursewareMeta', courseId);
|
||||||
|
|
||||||
const [expandAll, setExpandAll] = useState(false);
|
const [expandAll, setExpandAll] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -151,21 +158,27 @@ const OutlineTab = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<StartOrResumeCourseCard />
|
<StartOrResumeCourseCard />
|
||||||
<WelcomeMessage courseId={courseId} nextElementRef={expandButtonRef} />
|
<WelcomeMessage courseId={courseId} />
|
||||||
{rootCourseId && (
|
{rootCourseId && (
|
||||||
<>
|
<>
|
||||||
<div id="expand-button-row" className="row w-100 m-0 mb-3 justify-content-end">
|
<div 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 ref={expandButtonRef} variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
|
<Button 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>
|
||||||
<CourseHomeSectionOutlineSlot
|
<ol id="courseHome-outline" className="list-unstyled">
|
||||||
expandAll={expandAll}
|
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
||||||
sectionIds={courses[rootCourseId].sectionIds}
|
<Section
|
||||||
sections={sections}
|
key={sectionId}
|
||||||
/>
|
courseId={courseId}
|
||||||
|
defaultOpen={sections[sectionId].resumeBlock}
|
||||||
|
expand={expandAll}
|
||||||
|
section={sections[sectionId]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -181,7 +194,19 @@ const OutlineTab = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CourseTools />
|
<CourseTools />
|
||||||
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
<UpgradeNotification
|
||||||
|
offer={offer}
|
||||||
|
verifiedMode={verifiedMode}
|
||||||
|
accessExpiration={accessExpiration}
|
||||||
|
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||||
|
marketingUrl={marketingUrl}
|
||||||
|
upsellPageName="course_home"
|
||||||
|
userTimezone={userTimezone}
|
||||||
|
shouldDisplayBorder
|
||||||
|
timeOffsetMillis={timeOffsetMillis}
|
||||||
|
courseId={courseId}
|
||||||
|
org={org}
|
||||||
|
/>
|
||||||
<CourseDates />
|
<CourseDates />
|
||||||
<CourseHandouts />
|
<CourseHandouts />
|
||||||
</div>
|
</div>
|
||||||
@@ -191,4 +216,8 @@ const OutlineTab = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OutlineTab;
|
OutlineTab.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(OutlineTab);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React from 'react';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
@@ -54,7 +54,7 @@ describe('Outline Tab', () => {
|
|||||||
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
const 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().EXAMS_BASE_URL}/api/v1/student/course_id/${encodeURIComponent(courseId)}/onboarding?username=MockUser`;
|
const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}&username=MockUser`;
|
||||||
|
|
||||||
const store = initializeStore();
|
const store = initializeStore();
|
||||||
const defaultMetadata = Factory.build('courseHomeMetadata');
|
const defaultMetadata = Factory.build('courseHomeMetadata');
|
||||||
@@ -132,18 +132,7 @@ 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('org.openedx.frontend.learning.course_outline_tab_notifications.v1')).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' });
|
||||||
@@ -154,11 +143,11 @@ describe('Outline Tab', () => {
|
|||||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
// Click to expand section
|
// Click to expand section
|
||||||
await user.click(expandButton);
|
userEvent.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
|
||||||
await user.click(expandButton);
|
userEvent.click(expandButton);
|
||||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,7 +157,7 @@ describe('Outline Tab', () => {
|
|||||||
course_blocks: { blocks: courseBlocks.blocks },
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByLabelText('Completed section')).toBeInTheDocument();
|
expect(screen.getByTitle('Completed section')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays correct icon for incomplete assignment', async () => {
|
it('displays correct icon for incomplete assignment', async () => {
|
||||||
@@ -177,7 +166,7 @@ describe('Outline Tab', () => {
|
|||||||
course_blocks: { blocks: courseBlocks.blocks },
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByLabelText('Incomplete section')).toBeInTheDocument();
|
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SequenceLink displays link', async () => {
|
it('SequenceLink displays link', async () => {
|
||||||
@@ -276,50 +265,21 @@ 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();
|
||||||
|
|
||||||
await user.click(showMoreButton);
|
userEvent.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();
|
||||||
|
|
||||||
await user.click(showLessButton);
|
userEvent.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 () => {
|
|
||||||
setTabData({
|
|
||||||
welcome_message_html: '<p class="additional-spaces-in-tag" >'
|
|
||||||
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
|
|
||||||
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
|
|
||||||
+ 'Test welcome message that happens to be longer than one hundred words because of comments but displayed content is less.'
|
|
||||||
+ 'It should not be shortened.'
|
|
||||||
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
|
|
||||||
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
|
|
||||||
+ '</p>',
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
const showMoreButton = screen.queryByRole('button', { name: 'Show More' });
|
|
||||||
expect(showMoreButton).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not display if no update available', async () => {
|
it('does not display if no update available', async () => {
|
||||||
@@ -1190,6 +1150,80 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Upgrade Card', () => {
|
||||||
|
it('renders title when upgrade is available', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays link to upgrade', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('viewing upgrade card sends analytics', async () => {
|
||||||
|
sendTrackEvent.mockClear();
|
||||||
|
sendTrackingLogEvent.mockClear();
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendTrackEvent).toHaveBeenCalledWith('Promotion Viewed', {
|
||||||
|
org_key: 'edX',
|
||||||
|
courserun_key: courseId,
|
||||||
|
creative: 'sidebarupsell',
|
||||||
|
name: 'In-Course Verification Prompt',
|
||||||
|
position: 'sidebar-message',
|
||||||
|
promotion_id: 'courseware_verified_certificate_upsell',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', {
|
||||||
|
org_key: 'edX',
|
||||||
|
courserun_key: courseId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking upgrade link sends analytics', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
|
||||||
|
sendTrackEvent.mockClear();
|
||||||
|
sendTrackingLogEvent.mockClear();
|
||||||
|
const upgradeButton = screen.getByRole('link', { name: 'Upgrade for $149' });
|
||||||
|
|
||||||
|
fireEvent.click(upgradeButton);
|
||||||
|
|
||||||
|
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
||||||
|
expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', {
|
||||||
|
org_key: 'edX',
|
||||||
|
courserun_key: courseId,
|
||||||
|
creative: 'sidebarupsell',
|
||||||
|
name: 'In-Course Verification Prompt',
|
||||||
|
position: 'sidebar-message',
|
||||||
|
promotion_id: 'courseware_verified_certificate_upsell',
|
||||||
|
});
|
||||||
|
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', {
|
||||||
|
org_key: 'edX',
|
||||||
|
courserun_key: courseId,
|
||||||
|
linkCategory: 'green_upgrade',
|
||||||
|
linkName: 'course_home_green',
|
||||||
|
linkType: 'button',
|
||||||
|
pageName: 'course_home',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2);
|
||||||
|
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', {
|
||||||
|
org_key: 'edX',
|
||||||
|
courserun_key: courseId,
|
||||||
|
});
|
||||||
|
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', {
|
||||||
|
org_key: 'edX',
|
||||||
|
courserun_key: courseId,
|
||||||
|
location: 'sidebar-message',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Account Activation Alert', () => {
|
describe('Account Activation Alert', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const intersectionObserverMock = () => ({
|
const intersectionObserverMock = () => ({
|
||||||
@@ -1235,97 +1269,5 @@ describe('Outline Tab', () => {
|
|||||||
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
|
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
|
||||||
expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl);
|
expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('section should show hidden from toc message when hide_from_toc is true', async () => {
|
|
||||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
|
||||||
const courseBlocksIds = Object.keys(courseBlocks.blocks);
|
|
||||||
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
|
|
||||||
...blocks,
|
|
||||||
[blockId]: {
|
|
||||||
...courseBlocks.blocks[blockId],
|
|
||||||
hide_from_toc: true,
|
|
||||||
},
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
setTabData({
|
|
||||||
course_blocks: { blocks: newCourseBlocks },
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
|
|
||||||
const iconHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-icon');
|
|
||||||
const textHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-text');
|
|
||||||
expect(iconHiddenFromTocSectionNode).toBeInTheDocument();
|
|
||||||
expect(textHiddenFromTocSectionNode).toBeInTheDocument();
|
|
||||||
expect(textHiddenFromTocSectionNode.textContent).toBe('Hidden in Course Outline, accessible via link');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('section should not show hidden from toc message when hide_from_toc is false', async () => {
|
|
||||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
|
||||||
const courseBlocksIds = Object.keys(courseBlocks.blocks);
|
|
||||||
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
|
|
||||||
...blocks,
|
|
||||||
[blockId]: {
|
|
||||||
...courseBlocks.blocks[blockId],
|
|
||||||
hide_from_toc: false,
|
|
||||||
},
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
setTabData({
|
|
||||||
course_blocks: { blocks: newCourseBlocks },
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
|
|
||||||
const iconHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-icon');
|
|
||||||
const textHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-text');
|
|
||||||
|
|
||||||
expect(iconHiddenFromTocSectionNode).not.toBeInTheDocument();
|
|
||||||
expect(textHiddenFromTocSectionNode).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sequence link should show hidden from toc message when hide_from_toc is true', async () => {
|
|
||||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
|
||||||
const courseBlocksIds = Object.keys(courseBlocks.blocks);
|
|
||||||
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
|
|
||||||
...blocks,
|
|
||||||
[blockId]: {
|
|
||||||
...courseBlocks.blocks[blockId],
|
|
||||||
hide_from_toc: true,
|
|
||||||
},
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
setTabData({
|
|
||||||
course_blocks: { blocks: newCourseBlocks },
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
|
|
||||||
const iconHiddenFromTocSequenceLinkNode = screen.getByTestId('hide-from-toc-sequence-link-icon');
|
|
||||||
const textHiddenFromTocSequenceLink = screen.getByTestId('hide-from-toc-sequence-link-text');
|
|
||||||
expect(iconHiddenFromTocSequenceLinkNode).toBeInTheDocument();
|
|
||||||
expect(textHiddenFromTocSequenceLink).toBeInTheDocument();
|
|
||||||
expect(textHiddenFromTocSequenceLink.textContent).toBe('Subsections are not navigable between each other, they can only be accessed through their link.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sequence link not show hidden from toc message when hide_from_toc is false', async () => {
|
|
||||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
|
||||||
const courseBlocksIds = Object.keys(courseBlocks.blocks);
|
|
||||||
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
|
|
||||||
...blocks,
|
|
||||||
[blockId]: {
|
|
||||||
...courseBlocks.blocks[blockId],
|
|
||||||
hide_from_toc: false,
|
|
||||||
},
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
setTabData({
|
|
||||||
course_blocks: { blocks: newCourseBlocks },
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
|
|
||||||
const iconHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-icon');
|
|
||||||
const textHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-text');
|
|
||||||
|
|
||||||
expect(iconHiddenFromTocSequenceLink).not.toBeInTheDocument();
|
|
||||||
expect(textHiddenFromTocSequenceLink).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
123
src/course-home/outline-tab/Section.jsx
Normal file
123
src/course-home/outline-tab/Section.jsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Collapsible, IconButton } from '@edx/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 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,
|
||||||
|
} = 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="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-10 ml-3 p-0 font-weight-bold text-dark-500">
|
||||||
|
<span className="align-middle">{title}</span>
|
||||||
|
<span className="sr-only">
|
||||||
|
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
|
||||||
|
</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);
|
||||||
135
src/course-home/outline-tab/SequenceLink.jsx
Normal file
135
src/course-home/outline-tab/SequenceLink.jsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
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 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,
|
||||||
|
} = 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>
|
||||||
|
<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);
|
||||||
@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
|
|||||||
import {
|
import {
|
||||||
FormattedDate,
|
FormattedDate,
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
useIntl,
|
injectIntl,
|
||||||
|
intlShape,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Alert, Button } from '@openedx/paragon';
|
import { Alert, Button } from '@edx/paragon';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
@@ -24,8 +25,7 @@ export const CERT_STATUS_TYPE = {
|
|||||||
UNVERIFIED: 'unverified',
|
UNVERIFIED: 'unverified',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CertificateStatusAlert = ({ payload }) => {
|
const CertificateStatusAlert = ({ intl, payload }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
certificateAvailableDate,
|
certificateAvailableDate,
|
||||||
@@ -192,6 +192,7 @@ const CertificateStatusAlert = ({ payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
CertificateStatusAlert.propTypes = {
|
CertificateStatusAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
certificateAvailableDate: PropTypes.string,
|
certificateAvailableDate: PropTypes.string,
|
||||||
certStatus: PropTypes.string,
|
certStatus: PropTypes.string,
|
||||||
@@ -209,4 +210,4 @@ CertificateStatusAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CertificateStatusAlert;
|
export default injectIntl(CertificateStatusAlert);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
FormattedRelativeTime,
|
FormattedRelativeTime,
|
||||||
FormattedTime,
|
FormattedTime,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@openedx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
import { Info } from '@openedx/paragon/icons';
|
import { Info } from '@edx/paragon/icons';
|
||||||
|
|
||||||
const DAY_SEC = 24 * 60 * 60; // in seconds
|
const DAY_SEC = 24 * 60 * 60; // in seconds
|
||||||
const DAY_MS = DAY_SEC * 1000; // in ms
|
const DAY_MS = DAY_SEC * 1000; // in ms
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
import { Alert, Button, Hyperlink } from '@edx/paragon';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
@@ -14,8 +14,7 @@ import outlineMessages from '../../messages';
|
|||||||
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
const PrivateCourseAlert = ({ payload }) => {
|
const PrivateCourseAlert = ({ intl, payload }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
anonymousUser,
|
anonymousUser,
|
||||||
canEnroll,
|
canEnroll,
|
||||||
@@ -104,6 +103,7 @@ const PrivateCourseAlert = ({ payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
PrivateCourseAlert.propTypes = {
|
PrivateCourseAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
anonymousUser: PropTypes.bool,
|
anonymousUser: PropTypes.bool,
|
||||||
canEnroll: PropTypes.bool,
|
canEnroll: PropTypes.bool,
|
||||||
@@ -111,4 +111,4 @@ PrivateCourseAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PrivateCourseAlert;
|
export default injectIntl(PrivateCourseAlert);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert, Button } from '@openedx/paragon';
|
import { Alert, Button } from '@edx/paragon';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|||||||
@@ -36,16 +36,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Completed section',
|
defaultMessage: 'Completed section',
|
||||||
description: 'Text used to describe the green checkmark icon in front of a section title',
|
description: 'Text used to describe the green checkmark icon in front of a section title',
|
||||||
},
|
},
|
||||||
hiddenSection: {
|
|
||||||
id: 'learning.outline.hiddenSection',
|
|
||||||
defaultMessage: 'Hidden in Course Outline, accessible via link',
|
|
||||||
description: 'Label for hidden section in course outline',
|
|
||||||
},
|
|
||||||
hiddenSequenceLink: {
|
|
||||||
id: 'learning.outline.hiddenSequenceLink',
|
|
||||||
defaultMessage: 'Subsections are not navigable between each other, they can only be accessed through their link.',
|
|
||||||
description: 'Label for hidden sequence in course outline',
|
|
||||||
},
|
|
||||||
dates: {
|
dates: {
|
||||||
id: 'learning.outline.dates',
|
id: 'learning.outline.dates',
|
||||||
defaultMessage: 'Important dates',
|
defaultMessage: 'Important dates',
|
||||||
@@ -204,122 +194,122 @@ const messages = defineMessages({
|
|||||||
notStartedProctoringStatus: {
|
notStartedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.notStarted',
|
id: 'learning.proctoringPanel.status.notStarted',
|
||||||
defaultMessage: 'Not Started',
|
defaultMessage: 'Not Started',
|
||||||
description: 'It indcate that proctored onboarding exam hasn’t started yet',
|
description: 'It indcate that proctortrack onboarding exam hasn’t started yet',
|
||||||
},
|
},
|
||||||
startedProctoringStatus: {
|
startedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.started',
|
id: 'learning.proctoringPanel.status.started',
|
||||||
defaultMessage: 'Started',
|
defaultMessage: 'Started',
|
||||||
description: 'Label to indicate the starting status of the proctored onboarding exam',
|
description: 'Label to indicate the starting status of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
submittedProctoringStatus: {
|
submittedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.submitted',
|
id: 'learning.proctoringPanel.status.submitted',
|
||||||
defaultMessage: 'Submitted',
|
defaultMessage: 'Submitted',
|
||||||
description: 'Label to indicate the submitted status of proctored onboarding exam',
|
description: 'Label to indicate the submitted status of proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
verifiedProctoringStatus: {
|
verifiedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.verified',
|
id: 'learning.proctoringPanel.status.verified',
|
||||||
defaultMessage: 'Verified',
|
defaultMessage: 'Verified',
|
||||||
description: 'Label to indicate the verified status of the proctored onboarding exam',
|
description: 'Label to indicate the verified status of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
rejectedProctoringStatus: {
|
rejectedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.rejected',
|
id: 'learning.proctoringPanel.status.rejected',
|
||||||
defaultMessage: 'Rejected',
|
defaultMessage: 'Rejected',
|
||||||
description: 'Label to indicate the rejection status of the proctored onboarding exam',
|
description: 'Label to indicate the rejection status of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
errorProctoringStatus: {
|
errorProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.error',
|
id: 'learning.proctoringPanel.status.error',
|
||||||
defaultMessage: 'Error',
|
defaultMessage: 'Error',
|
||||||
description: 'Label to indicate that there is error in proctored onboarding exam',
|
description: 'Label to indicate that there is error in proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
otherCourseApprovedProctoringStatus: {
|
otherCourseApprovedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.otherCourseApproved',
|
id: 'learning.proctoringPanel.status.otherCourseApproved',
|
||||||
defaultMessage: 'Approved in Another Course',
|
defaultMessage: 'Approved in Another Course',
|
||||||
description: 'Label to indicate that the proctored onboarding exam is verified based on taking onboarding exam on another course',
|
description: 'Label to indicate that the proctortrack onboarding exam is verified based on taking onboarding exam on another course',
|
||||||
},
|
},
|
||||||
expiringSoonProctoringStatus: {
|
expiringSoonProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.expiringSoon',
|
id: 'learning.proctoringPanel.status.expiringSoon',
|
||||||
defaultMessage: 'Expiring Soon',
|
defaultMessage: 'Expiring Soon',
|
||||||
description: 'A label to indicate that proctored onboarding exam will expire soon',
|
description: 'A label to indicate that proctortrack onboarding exam will expire soon',
|
||||||
},
|
},
|
||||||
expiredProctoringStatus: {
|
expiredProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.expired',
|
id: 'learning.proctoringPanel.status.expired',
|
||||||
defaultMessage: 'Expired',
|
defaultMessage: 'Expired',
|
||||||
description: 'A label to indicate that proctored onboarding exam has expired',
|
description: 'A label to indicate that proctortrack onboarding exam has expired',
|
||||||
},
|
},
|
||||||
proctoringCurrentStatus: {
|
proctoringCurrentStatus: {
|
||||||
id: 'learning.proctoringPanel.status',
|
id: 'learning.proctoringPanel.status',
|
||||||
defaultMessage: 'Current Onboarding Status:',
|
defaultMessage: 'Current Onboarding Status:',
|
||||||
description: 'The text that precede the status label of proctored onboarding exam',
|
description: 'The text that precede the status label of proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
notStartedProctoringMessage: {
|
notStartedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.notStarted',
|
id: 'learning.proctoringPanel.message.notStarted',
|
||||||
defaultMessage: 'You have not started your onboarding exam.',
|
defaultMessage: 'You have not started your onboarding exam.',
|
||||||
description: 'The text that explain the meaning of (not started) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (not started) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
startedProctoringMessage: {
|
startedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.started',
|
id: 'learning.proctoringPanel.message.started',
|
||||||
defaultMessage: 'You have started your onboarding exam.',
|
defaultMessage: 'You have started your onboarding exam.',
|
||||||
description: 'The text that explain the meaning of (started) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (started) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
submittedProctoringMessage: {
|
submittedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.submitted',
|
id: 'learning.proctoringPanel.message.submitted',
|
||||||
defaultMessage: 'You have submitted your onboarding exam.',
|
defaultMessage: 'You have submitted your onboarding exam.',
|
||||||
description: 'The text that explain the meaning of (submitted) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (submitted) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
verifiedProctoringMessage: {
|
verifiedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.verified',
|
id: 'learning.proctoringPanel.message.verified',
|
||||||
defaultMessage: 'Your onboarding exam has been approved in this course.',
|
defaultMessage: 'Your onboarding exam has been approved in this course.',
|
||||||
description: 'The text that explain the meaning of (verified) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (verified) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
rejectedProctoringMessage: {
|
rejectedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.rejected',
|
id: 'learning.proctoringPanel.message.rejected',
|
||||||
defaultMessage: 'Your onboarding exam has been rejected. Please retry onboarding.',
|
defaultMessage: 'Your onboarding exam has been rejected. Please retry onboarding.',
|
||||||
description: 'The text that explain the meaning of (rejected) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (rejected) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
errorProctoringMessage: {
|
errorProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.error',
|
id: 'learning.proctoringPanel.message.error',
|
||||||
defaultMessage: 'An error has occurred during your onboarding exam. Please retry onboarding.',
|
defaultMessage: 'An error has occurred during your onboarding exam. Please retry onboarding.',
|
||||||
description: 'The text that explain the meaning of (error) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (error) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
otherCourseApprovedProctoringMessage: {
|
otherCourseApprovedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.otherCourseApproved',
|
id: 'learning.proctoringPanel.message.otherCourseApproved',
|
||||||
defaultMessage: 'Your onboarding exam has been approved in another course.',
|
defaultMessage: 'Your onboarding exam has been approved in another course.',
|
||||||
description: 'The text that explain the meaning of (approved in another course) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (approved in another course) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
otherCourseApprovedProctoringDetail: {
|
otherCourseApprovedProctoringDetail: {
|
||||||
id: 'learning.proctoringPanel.detail.otherCourseApproved',
|
id: 'learning.proctoringPanel.detail.otherCourseApproved',
|
||||||
defaultMessage: 'If your device has changed, we recommend that you complete this course\'s onboarding exam in order to ensure that your setup still meets the requirements for proctoring.',
|
defaultMessage: 'If your device has changed, we recommend that you complete this course\'s onboarding exam in order to ensure that your setup still meets the requirements for proctoring.',
|
||||||
description: 'The text that recommend an action when the status of the proctored onboarding exam is (approved in another course)',
|
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (approved in another course)',
|
||||||
},
|
},
|
||||||
expiringSoonProctoringMessage: {
|
expiringSoonProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.expiringSoon',
|
id: 'learning.proctoringPanel.message.expiringSoon',
|
||||||
defaultMessage: 'Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
defaultMessage: 'Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
||||||
description: 'The text that recommend an action when the status of the proctored onboarding exam is (expiring soon)',
|
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expiring soon)',
|
||||||
},
|
},
|
||||||
expiredProctoringMessage: {
|
expiredProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.expired',
|
id: 'learning.proctoringPanel.message.expired',
|
||||||
defaultMessage: 'Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.',
|
defaultMessage: 'Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.',
|
||||||
description: 'The text that recommend an action when the status of the proctored onboarding exam is (expired)',
|
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expired)',
|
||||||
},
|
},
|
||||||
proctoringPanelGeneralInfo: {
|
proctoringPanelGeneralInfo: {
|
||||||
id: 'learning.proctoringPanel.generalInfo',
|
id: 'learning.proctoringPanel.generalInfo',
|
||||||
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
||||||
description: 'It indicate key and important fact to learner about the importance of taking proctored onboarding exam',
|
description: 'It indicate key and important fact to learner about the importance of taking proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringPanelGeneralInfoSubmitted: {
|
proctoringPanelGeneralInfoSubmitted: {
|
||||||
id: 'learning.proctoringPanel.generalInfoSubmitted',
|
id: 'learning.proctoringPanel.generalInfoSubmitted',
|
||||||
defaultMessage: 'Your submitted profile is in review.',
|
defaultMessage: 'Your submitted profile is in review.',
|
||||||
description: 'The text that explain the meaning of (in review) label of the proctored onboarding exam',
|
description: 'The text that explain the meaning of (in review) label of the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringPanelGeneralTime: {
|
proctoringPanelGeneralTime: {
|
||||||
id: 'learning.proctoringPanel.generalTime',
|
id: 'learning.proctoringPanel.generalTime',
|
||||||
defaultMessage: 'Onboarding profile review can take 2+ business days.',
|
defaultMessage: 'Onboarding profile review can take 2+ business days.',
|
||||||
description: 'This text explain for how long the (in review) status of the proctored onboarding exam might remain',
|
description: 'This text explain for how long the (in review) status of the proctortrack onboarding exam might remain',
|
||||||
},
|
},
|
||||||
proctoringOnboardingButton: {
|
proctoringOnboardingButton: {
|
||||||
id: 'learning.proctoringPanel.onboardingButton',
|
id: 'learning.proctoringPanel.onboardingButton',
|
||||||
defaultMessage: 'Complete Onboarding',
|
defaultMessage: 'Complete Onboarding',
|
||||||
description: 'Text shown on the button that starts the actual proctored onboarding exam when it is released',
|
description: 'Text shown on the button that starts the actual proctortrack onboarding exam when it is released',
|
||||||
},
|
},
|
||||||
proctoringOnboardingPracticeButton: {
|
proctoringOnboardingPracticeButton: {
|
||||||
id: 'learning.proctoringPanel.onboardingPracticeButton',
|
id: 'learning.proctoringPanel.onboardingPracticeButton',
|
||||||
@@ -329,27 +319,17 @@ const messages = defineMessages({
|
|||||||
proctoringOnboardingButtonNotOpen: {
|
proctoringOnboardingButtonNotOpen: {
|
||||||
id: 'learning.proctoringPanel.onboardingButtonNotOpen',
|
id: 'learning.proctoringPanel.onboardingButtonNotOpen',
|
||||||
defaultMessage: 'Onboarding Opens: {releaseDate}',
|
defaultMessage: 'Onboarding Opens: {releaseDate}',
|
||||||
description: 'It indicate when or from when the learner can take the proctored onboarding exam',
|
description: 'It indicate when or from when the learner can take the proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringReviewRequirementsButton: {
|
proctoringReviewRequirementsButton: {
|
||||||
id: 'learning.proctoringPanel.reviewRequirementsButton',
|
id: 'learning.proctoringPanel.reviewRequirementsButton',
|
||||||
defaultMessage: 'Review instructions and system requirements',
|
defaultMessage: 'Review instructions and system requirements',
|
||||||
description: 'Anchor text for button which redirect leaner to doc or a detailed page about proctored onboarding exam',
|
description: 'Anchor text for button which redirect leaner to doc or a detailed page about proctortrack onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringOnboardingButtonPastDue: {
|
proctoringOnboardingButtonPastDue: {
|
||||||
id: 'learning.proctoringPanel.onboardingButtonPastDue',
|
id: 'learning.proctoringPanel.onboardingButtonPastDue',
|
||||||
defaultMessage: 'Onboarding Past Due',
|
defaultMessage: 'Onboarding Past Due',
|
||||||
description: 'Text that show when the deadline of proctored 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',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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,14 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import DateSummary from '../DateSummary';
|
import DateSummary from '../DateSummary';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
const CourseDates = () => {
|
const CourseDates = ({
|
||||||
const intl = useIntl();
|
intl,
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -39,7 +40,7 @@ const CourseDates = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
<a id="dates-tab-link" className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
<a className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
||||||
{intl.formatMessage(messages.allDates)}
|
{intl.formatMessage(messages.allDates)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,4 +48,8 @@ const CourseDates = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CourseDates;
|
CourseDates.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CourseDates);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import LmsHtmlFragment from '../LmsHtmlFragment';
|
import LmsHtmlFragment from '../LmsHtmlFragment';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
const CourseHandouts = () => {
|
const CourseHandouts = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -32,4 +31,8 @@ const CourseHandouts = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CourseHandouts;
|
CourseHandouts.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CourseHandouts);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
|||||||
|
|
||||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
faBookmark, faCertificate, faInfo, faCalendar, faStar,
|
faBookmark, faCertificate, faInfo, faCalendar, faStar,
|
||||||
@@ -14,8 +14,7 @@ import messages from '../messages';
|
|||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
||||||
|
|
||||||
const CourseTools = () => {
|
const CourseTools = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -82,4 +81,8 @@ const CourseTools = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CourseTools;
|
CourseTools.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CourseTools);
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
|
@import "~@edx/brand/paragon/variables";
|
||||||
|
@import "~@edx/paragon/scss/core/core";
|
||||||
|
@import "~@edx/brand/paragon/overrides";
|
||||||
|
|
||||||
.flag-button {
|
.flag-button {
|
||||||
background-color: var(--pgn-color-white);
|
background-color: $white;
|
||||||
border: 1px solid var(--pgn-color-light-400);
|
border: 1px solid $light-400;
|
||||||
border-radius: .2rem;
|
border-radius: .2rem;
|
||||||
box-shadow: 0 0 0 2px var(--pgn-color-light-400);
|
box-shadow: 0 0 0 2px $light-400;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid var(--pgn-color-primary-300);
|
border: 1px solid $primary-300;
|
||||||
box-shadow: 0 0 0 2px var(--pgn-color-white);
|
box-shadow: 0 0 0 2px $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flag-button-selected {
|
.flag-button-selected {
|
||||||
border: 1px solid var(--pgn-color-primary-300);
|
border: 1px solid $primary-300;
|
||||||
box-shadow: 0 0 0 2px var(--pgn-color-primary-300);
|
box-shadow: 0 0 0 2px $primary-300;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
// These flag svgs are derivatives of the Flag icon from paragon
|
// These flag svgs are derivatives of the Flag icon from paragon
|
||||||
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
|
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
|
||||||
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
||||||
@@ -13,8 +13,8 @@ const LearningGoalButton = ({
|
|||||||
level,
|
level,
|
||||||
isSelected,
|
isSelected,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
|
intl,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
|
||||||
const buttonDetails = {
|
const buttonDetails = {
|
||||||
casual: {
|
casual: {
|
||||||
daysPerWeek: 1,
|
daysPerWeek: 1,
|
||||||
@@ -53,6 +53,7 @@ LearningGoalButton.propTypes = {
|
|||||||
level: PropTypes.string.isRequired,
|
level: PropTypes.string.isRequired,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
handleSelect: PropTypes.func.isRequired,
|
handleSelect: PropTypes.func.isRequired,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LearningGoalButton;
|
export default injectIntl(LearningGoalButton);
|
||||||
|
|||||||
@@ -2,17 +2,15 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import camelCase from 'lodash.camelcase';
|
import camelCase from 'lodash.camelcase';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { getExternalLinkUrl } from '@edx/frontend-platform';
|
import { Button } from '@edx/paragon';
|
||||||
import { Button } from '@openedx/paragon';
|
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { getProctoringInfoData } from '../../data/api';
|
import { getProctoringInfoData } from '../../data/api';
|
||||||
import { fetchProctoringInfoResolved } from '../../data/slice';
|
import { fetchProctoringInfoResolved } from '../../data/slice';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
const ProctoringInfoPanel = () => {
|
const ProctoringInfoPanel = ({ intl }) => {
|
||||||
const intl = useIntl();
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -208,7 +206,7 @@ const ProctoringInfoPanel = () => {
|
|||||||
{isSubmissionRequired(readableStatus) && (
|
{isSubmissionRequired(readableStatus) && (
|
||||||
onboardingExamButton
|
onboardingExamButton
|
||||||
)}
|
)}
|
||||||
<Button variant="outline-primary" block href={getExternalLinkUrl('https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams')}>
|
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
||||||
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
|
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,4 +216,8 @@ const ProctoringInfoPanel = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProctoringInfoPanel;
|
ProctoringInfoPanel.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ProctoringInfoPanel);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.outline-sidebar-proctoring-panel {
|
.outline-sidebar-proctoring-panel {
|
||||||
border: 1px solid var(--pgn-color-dark-500);
|
border: 1px solid $dark-500;
|
||||||
border-top: 5px solid var(--pgn-color-brand-600);
|
border-top: 5px solid $brand-600;
|
||||||
}
|
}
|
||||||
.proctoring-onboarding-success {
|
.proctoring-onboarding-success {
|
||||||
border-top: 5px solid var(--pgn-color-primary-500);
|
border-top: 5px solid $primary-500;
|
||||||
}
|
}
|
||||||
.proctoring-onboarding-submitted {
|
.proctoring-onboarding-submitted {
|
||||||
border-top: 5px solid var(--pgn-color-dark-500);
|
border-top: 5px solid $dark-500;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user