Compare commits
116 Commits
open-relea
...
ags/fronte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398330fa07 | ||
|
|
f92fc8c3a5 | ||
|
|
5e072949d6 | ||
|
|
2d132f114c | ||
|
|
c73ef26d8e | ||
|
|
97ca7fe6aa | ||
|
|
e95a59c6c8 | ||
|
|
5f9c441cd2 | ||
|
|
2e641ac6c9 | ||
|
|
22937918ab | ||
|
|
714f5d452c | ||
|
|
8ac9745261 | ||
|
|
340580cb41 | ||
|
|
5a99ca5c91 | ||
|
|
9943df49e4 | ||
|
|
855474d406 | ||
|
|
a78496a3f6 | ||
|
|
79b65dadca | ||
|
|
fc8f5d43e8 | ||
|
|
6232f40a74 | ||
|
|
bc0ff1ce65 | ||
|
|
5997b29cee | ||
|
|
d2de0632cd | ||
|
|
922cc2187a | ||
|
|
d9539796b5 | ||
|
|
e0acb501eb | ||
|
|
a03ffe2724 | ||
|
|
cbdf7ce064 | ||
|
|
7184e85b2b | ||
|
|
b5321d01e4 | ||
|
|
6c8ab1a4c9 | ||
|
|
01f9d8f50b | ||
|
|
764befd4bd | ||
|
|
7317c9424a | ||
|
|
d897663b73 | ||
|
|
2e4eb158f2 | ||
|
|
35b229bd1b | ||
|
|
4ebd569792 | ||
|
|
52235ebc1c | ||
|
|
aa380e8619 | ||
|
|
4cf0c7f4d7 | ||
|
|
743650a99e | ||
|
|
39d89bee9e | ||
|
|
a601e431b2 | ||
|
|
7519bbe28e | ||
|
|
4b90dcbfc3 | ||
|
|
54cb52cb6d | ||
|
|
6dbd3f49dd | ||
|
|
678502bb40 | ||
|
|
bf77fc7ca1 | ||
|
|
421a9a5d2b | ||
|
|
dfe44cae56 | ||
|
|
a88571dae8 | ||
|
|
a4ea334692 | ||
|
|
97a1cb4ffc | ||
|
|
5166bfe056 | ||
|
|
33e3765b19 | ||
|
|
a13e7d7389 | ||
|
|
a4ea1b54a4 | ||
|
|
cd430ebb5d | ||
|
|
630d44a8cc | ||
|
|
894e16ddf0 | ||
|
|
263c486330 | ||
|
|
b3d33667d4 | ||
|
|
b500546e8d | ||
|
|
cb9e0aa52f | ||
|
|
69ff5463b3 | ||
|
|
3b4561e142 | ||
|
|
cf3b3a27bc | ||
|
|
3bb7aa06bc | ||
|
|
4cea9e582b | ||
|
|
0c74bb5106 | ||
|
|
b082f3ed19 | ||
|
|
5d477cebb2 | ||
|
|
851e49f8fb | ||
|
|
09436dd175 | ||
|
|
53c8e01c28 | ||
|
|
ed2d816bbe | ||
|
|
7c067299fb | ||
|
|
4ee1570bfa | ||
|
|
91c548847b | ||
|
|
49440ffb45 | ||
|
|
6752447d94 | ||
|
|
75c6aadb09 | ||
|
|
9eceb355f6 | ||
|
|
df7786388c | ||
|
|
361de31e22 | ||
|
|
9e040ec8f1 | ||
|
|
8db8aeed71 | ||
|
|
04471e550b | ||
|
|
925ee97a76 | ||
|
|
65086af173 | ||
|
|
33923d9a69 | ||
|
|
080d31e934 | ||
|
|
f3c80ed39b | ||
|
|
1ca4eda08a | ||
|
|
6193c2d1b3 | ||
|
|
f8a1147571 | ||
|
|
edba1600dc | ||
|
|
9a07ad1501 | ||
|
|
b343ca7a74 | ||
|
|
b6d272e99d | ||
|
|
0fbb53ae86 | ||
|
|
ba06fd7c98 | ||
|
|
9396fbd9d4 | ||
|
|
57d880de70 | ||
|
|
bfad5cf684 | ||
|
|
b0378e1331 | ||
|
|
19d06d60be | ||
|
|
df91fef82e | ||
|
|
7e53ddb685 | ||
|
|
be72e36a3a | ||
|
|
fa5cf8f204 | ||
|
|
759d154e13 | ||
|
|
7c4200e9d3 | ||
|
|
e5e73e40ba |
3
.env
3
.env
@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL=''
|
|||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||||
|
EXAMS_BASE_URL=''
|
||||||
FAVICON_URL=''
|
FAVICON_URL=''
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
INSIGHTS_BASE_URL=''
|
INSIGHTS_BASE_URL=''
|
||||||
@@ -28,6 +29,8 @@ LOGO_WHITE_URL=''
|
|||||||
LEGACY_THEME_NAME=''
|
LEGACY_THEME_NAME=''
|
||||||
MARKETING_SITE_BASE_URL=''
|
MARKETING_SITE_BASE_URL=''
|
||||||
ORDER_HISTORY_URL=''
|
ORDER_HISTORY_URL=''
|
||||||
|
PROCTORED_EXAM_FAQ_URL=''
|
||||||
|
PROCTORED_EXAM_RULES_URL=''
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||||
SEARCH_CATALOG_URL=''
|
SEARCH_CATALOG_URL=''
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
|
|||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
|
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=''
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||||
@@ -28,6 +29,8 @@ LEGACY_THEME_NAME=''
|
|||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||||
PORT=2000
|
PORT=2000
|
||||||
|
PROCTORED_EXAM_FAQ_URL=''
|
||||||
|
PROCTORED_EXAM_RULES_URL=''
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
|
|||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
|
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=''
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||||
@@ -28,6 +29,8 @@ LEGACY_THEME_NAME=''
|
|||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||||
PORT=2000
|
PORT=2000
|
||||||
|
PROCTORED_EXAM_FAQ_URL=''
|
||||||
|
PROCTORED_EXAM_RULES_URL=''
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
|
|||||||
22
.eslintrc.js
22
.eslintrc.js
@@ -1,11 +1,17 @@
|
|||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { createConfig } = require('@edx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('eslint', {
|
const config = createConfig('eslint', {
|
||||||
overrides: [{
|
rules: {
|
||||||
files: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)", "setupTest.js"],
|
// TODO: all these rules should be renabled/addressed. temporarily turned off to unblock a release.
|
||||||
rules: {
|
'react-hooks/rules-of-hooks': 'off',
|
||||||
'import/named': 'off',
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
'import/no-extraneous-dependencies': 'off',
|
'import/no-extraneous-dependencies': 'off',
|
||||||
},
|
'no-restricted-exports': 'off',
|
||||||
}],
|
'react/jsx-no-useless-fragment': 'off',
|
||||||
|
'react/no-unknown-property': 'off',
|
||||||
|
'func-names': 'off',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|||||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "label: " it tries to apply
|
||||||
|
# the label indicated in rest of comment.
|
||||||
|
# If the comment starts with "remove label: ", it tries
|
||||||
|
# to remove the indicated label.
|
||||||
|
# Note: Labels are allowed to have spaces and this script does
|
||||||
|
# not parse spaces (as often a space is legitimate), so the command
|
||||||
|
# "label: really long lots of words label" will apply the
|
||||||
|
# label "really long lots of words label"
|
||||||
|
|
||||||
|
name: Allows for the adding and removing of labels via comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add_remove_labels:
|
||||||
|
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||||
|
|
||||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version-check:
|
version-check:
|
||||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||||
|
|||||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "assign me" it assigns the author to the
|
||||||
|
# ticket (case insensitive)
|
||||||
|
|
||||||
|
name: Assign comment author to ticket if they say "assign me"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
self_assign_by_comment:
|
||||||
|
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||||
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: Update Browserslist DB
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-browserslist:
|
||||||
|
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
|
||||||
7
.github/workflows/validate.yml
vendored
7
.github/workflows/validate.yml
vendored
@@ -9,14 +9,13 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node: [16]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- name: Setup Nodejs Env
|
||||||
|
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ env.NODE_VER }}
|
||||||
- run: make validate.ci
|
- run: make validate.ci
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintcache
|
.eslintcache
|
||||||
.idea
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
coverage
|
coverage
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -1,6 +1,7 @@
|
|||||||
export TRANSIFEX_RESOURCE=frontend-app-learning
|
export TRANSIFEX_RESOURCE=frontend-app-learning
|
||||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
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
|
||||||
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
|
||||||
@@ -42,9 +43,24 @@ push_translations:
|
|||||||
# Pushing comments to Transifex...
|
# Pushing comments to Transifex...
|
||||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||||
|
|
||||||
|
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||||
# Pulls translations from Transifex.
|
# Pulls translations from Transifex.
|
||||||
pull_translations:
|
pull_translations:
|
||||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||||
|
else
|
||||||
|
# Experimental: OEP-58 Pulls translations using atlas
|
||||||
|
pull_translations:
|
||||||
|
rm -rf src/i18n/messages
|
||||||
|
mkdir src/i18n/messages
|
||||||
|
cd src/i18n/messages \
|
||||||
|
&& atlas pull --filter=$(transifex_langs) \
|
||||||
|
translations/paragon/src/i18n/messages:paragon \
|
||||||
|
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||||
|
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||||
|
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
|
||||||
|
|
||||||
|
$(intl_imports) paragon frontend-component-header frontend-component-footer 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:
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ module.exports = createConfig('jest', {
|
|||||||
'src/i18n',
|
'src/i18n',
|
||||||
'src/.*\\.exp\\..*',
|
'src/.*\\.exp\\..*',
|
||||||
],
|
],
|
||||||
|
testTimeout: 30000,
|
||||||
|
testEnvironment: 'jsdom'
|
||||||
});
|
});
|
||||||
|
|||||||
49482
package-lock.json
generated
49482
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -29,51 +29,53 @@
|
|||||||
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||||
"@edx/frontend-component-footer": "11.1.0",
|
"@edx/frontend-component-footer": "12.0.0",
|
||||||
"@edx/frontend-component-header": "3.1.0",
|
"@edx/frontend-component-header": "4.0.0",
|
||||||
"@edx/frontend-lib-special-exams": "2.1.0",
|
"@edx/frontend-lib-special-exams": "2.19.1",
|
||||||
"@edx/frontend-platform": "2.5.1",
|
"@edx/frontend-platform": "4.3.0",
|
||||||
"@edx/paragon": "19.18.3",
|
"@edx/paragon": "20.28.4",
|
||||||
|
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.5.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.3.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.18",
|
"@fortawesome/react-fontawesome": "0.1.18",
|
||||||
"@popperjs/core": "2.11.5",
|
"@popperjs/core": "2.11.8",
|
||||||
"@reduxjs/toolkit": "1.8.1",
|
"@reduxjs/toolkit": "1.8.1",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.2",
|
||||||
"core-js": "3.22.2",
|
"core-js": "3.22.2",
|
||||||
"history": "^5.3.0",
|
"history": "5.3.0",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"lodash.camelcase": "4.3.0",
|
"lodash.camelcase": "4.3.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.3",
|
||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-redux": "7.2.8",
|
"react-redux": "7.2.9",
|
||||||
"react-router": "5.2.1",
|
"react-router": "5.2.1",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router-dom": "5.3.0",
|
||||||
"react-share": "4.4.0",
|
"react-share": "4.4.1",
|
||||||
"redux": "4.1.2",
|
"redux": "4.1.2",
|
||||||
"regenerator-runtime": "0.13.9",
|
"regenerator-runtime": "0.13.11",
|
||||||
"reselect": "4.1.5",
|
"reselect": "4.1.7",
|
||||||
"truncate-html": "1.0.4",
|
"truncate-html": "1.0.4",
|
||||||
"util": "0.12.4"
|
"util": "0.12.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/browserslist-config": "1.0.2",
|
"@edx/browserslist-config": "1.1.1",
|
||||||
"@edx/frontend-build": "9.1.4",
|
"@edx/frontend-build": "^12.8.27",
|
||||||
"@edx/reactifex": "2.0.1",
|
"@edx/reactifex": "2.1.1",
|
||||||
"@pact-foundation/pact": "9.17.3",
|
"@pact-foundation/pact": "^11.0.2",
|
||||||
"@testing-library/jest-dom": "5.16.4",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@testing-library/user-event": "13.5.0",
|
"@testing-library/user-event": "13.5.0",
|
||||||
"axios-mock-adapter": "1.20.0",
|
"axios-mock-adapter": "1.20.0",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"es-check": "6.2.1",
|
"es-check": "6.2.1",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest": "27.5.1",
|
"jest": "29.5.0",
|
||||||
"rosie": "2.1.0"
|
"rosie": "2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
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 {
|
import {
|
||||||
@@ -8,18 +7,8 @@ import { Alert, Hyperlink } from '@edx/paragon';
|
|||||||
import { Info } from '@edx/paragon/icons';
|
import { Info } from '@edx/paragon/icons';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import AccessExpirationAlertMMP2P from './AccessExpirationAlertMMP2P';
|
|
||||||
|
|
||||||
function AccessExpirationAlert({ intl, payload }) {
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
const [showMMP2P, setShowMMP2P] = useState(!!window.experiment__home_alert_bShowMMP2P);
|
|
||||||
if (window.experiment__home_alert_showMMP2P === undefined) {
|
|
||||||
window.experiment__home_alert_showMMP2P = (val) => {
|
|
||||||
window.experiment__home_alert_bShowMMP2P = !!val;
|
|
||||||
setShowMMP2P(!!val);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const AccessExpirationAlert = ({ intl, payload }) => {
|
||||||
const {
|
const {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
courseId,
|
courseId,
|
||||||
@@ -39,13 +28,6 @@ function AccessExpirationAlert({ intl, payload }) {
|
|||||||
upgradeUrl,
|
upgradeUrl,
|
||||||
} = accessExpiration;
|
} = accessExpiration;
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
if (showMMP2P) {
|
|
||||||
return (
|
|
||||||
<AccessExpirationAlertMMP2P payload={payload} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const logClick = () => {
|
const logClick = () => {
|
||||||
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
|
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
|
||||||
org_key: org,
|
org_key: org,
|
||||||
@@ -134,7 +116,7 @@ function AccessExpirationAlert({ intl, payload }) {
|
|||||||
{deadlineMessage}
|
{deadlineMessage}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
AccessExpirationAlert.propTypes = {
|
AccessExpirationAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { FormattedDate, injectIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Alert, Hyperlink } from '@edx/paragon';
|
|
||||||
import { Info } from '@edx/paragon/icons';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
function AccessExpirationAlertMMP2P({ payload }) {
|
|
||||||
const {
|
|
||||||
accessExpiration,
|
|
||||||
userTimezone,
|
|
||||||
} = payload;
|
|
||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
|
||||||
|
|
||||||
if (!accessExpiration) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
expirationDate,
|
|
||||||
upgradeDeadline,
|
|
||||||
upgradeUrl,
|
|
||||||
} = accessExpiration;
|
|
||||||
|
|
||||||
let deadlineMessage = null;
|
|
||||||
const formatDate = (val, key) => (
|
|
||||||
<FormattedDate
|
|
||||||
key={`accessExpiration.${key}`}
|
|
||||||
day="numeric"
|
|
||||||
month="short"
|
|
||||||
year="numeric"
|
|
||||||
value={val}
|
|
||||||
{...timezoneFormatArgs}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (upgradeDeadline && upgradeUrl) {
|
|
||||||
deadlineMessage = (
|
|
||||||
<>
|
|
||||||
Upgrade by {formatDate(upgradeDeadline, 'upgradeDesc')} to unlock unlimited access to all course activities, including graded assignments.
|
|
||||||
|
|
||||||
<Hyperlink
|
|
||||||
className="font-weight-bold"
|
|
||||||
style={{ textDecoration: 'underline' }}
|
|
||||||
destination={upgradeUrl}
|
|
||||||
>
|
|
||||||
{messages.upgradeNow.defaultMessage}
|
|
||||||
</Hyperlink>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Alert variant="info" icon={Info}>
|
|
||||||
<span className="font-weight-bold">
|
|
||||||
Unlock full course content by {formatDate(upgradeDeadline, 'upgradeTitle')}
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
{deadlineMessage}
|
|
||||||
<br />
|
|
||||||
You lose all access to the first two weeks of scheduled content
|
|
||||||
on {formatDate(expirationDate, 'expirationBody')}.
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessExpirationAlertMMP2P.propTypes = {
|
|
||||||
payload: PropTypes.shape({
|
|
||||||
accessExpiration: PropTypes.shape({
|
|
||||||
expirationDate: PropTypes.string.isRequired,
|
|
||||||
masqueradingExpiredCourse: PropTypes.bool.isRequired,
|
|
||||||
upgradeDeadline: PropTypes.string,
|
|
||||||
upgradeUrl: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
userTimezone: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(AccessExpirationAlertMMP2P);
|
|
||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||||
import { PageBanner } from '@edx/paragon';
|
import { PageBanner } from '@edx/paragon';
|
||||||
|
|
||||||
function AccessExpirationMasqueradeBanner({ payload }) {
|
const AccessExpirationMasqueradeBanner = ({ payload }) => {
|
||||||
const {
|
const {
|
||||||
expirationDate,
|
expirationDate,
|
||||||
userTimezone,
|
userTimezone,
|
||||||
@@ -27,7 +27,7 @@ function AccessExpirationMasqueradeBanner({ payload }) {
|
|||||||
/>
|
/>
|
||||||
</PageBanner>
|
</PageBanner>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
AccessExpirationMasqueradeBanner.propTypes = {
|
AccessExpirationMasqueradeBanner.propTypes = {
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ const AccessExpirationMasqueradeBanner = React.lazy(() => import('./AccessExpira
|
|||||||
|
|
||||||
function useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, topic, analyticsPageName) {
|
function useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, topic, analyticsPageName) {
|
||||||
const isVisible = accessExpiration && !accessExpiration.masqueradingExpiredCourse; // If it exists, show it.
|
const isVisible = accessExpiration && !accessExpiration.masqueradingExpiredCourse; // If it exists, show it.
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
courseId,
|
courseId,
|
||||||
org,
|
org,
|
||||||
userTimezone,
|
userTimezone,
|
||||||
analyticsPageName,
|
analyticsPageName,
|
||||||
};
|
}), [accessExpiration, analyticsPageName, courseId, org, userTimezone]);
|
||||||
|
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientAccessExpirationAlert',
|
code: 'clientAccessExpirationAlert',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic,
|
topic,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,14 +34,14 @@ export function useAccessExpirationMasqueradeBanner(courseId, tab) {
|
|||||||
|
|
||||||
const isVisible = accessExpiration && accessExpiration.masqueradingExpiredCourse;
|
const isVisible = accessExpiration && accessExpiration.masqueradingExpiredCourse;
|
||||||
const expirationDate = accessExpiration && accessExpiration.expirationDate;
|
const expirationDate = accessExpiration && accessExpiration.expirationDate;
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
expirationDate,
|
expirationDate,
|
||||||
userTimezone,
|
userTimezone,
|
||||||
};
|
}), [expirationDate, userTimezone]);
|
||||||
|
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientAccessExpirationMasqueradeBanner',
|
code: 'clientAccessExpirationMasqueradeBanner',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'instructor-toolbar-alerts',
|
topic: 'instructor-toolbar-alerts',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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';
|
||||||
|
|
||||||
function ActiveEnterpriseAlert({ intl, payload }) {
|
const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
||||||
const { text, courseId } = payload;
|
const { text, courseId } = payload;
|
||||||
const changeActiveEnterprise = (
|
const changeActiveEnterprise = (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
@@ -35,7 +35,7 @@ function ActiveEnterpriseAlert({ intl, payload }) {
|
|||||||
/>
|
/>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ActiveEnterpriseAlert.propTypes = {
|
ActiveEnterpriseAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ describe('ActiveEnterpriseAlert', () => {
|
|||||||
render(<ActiveEnterpriseAlert {...mockData} />);
|
render(<ActiveEnterpriseAlert {...mockData} />);
|
||||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||||
expect(screen.getByText('test message', { exact: false })).toBeInTheDocument();
|
expect(screen.getByText('test message', { exact: false })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('link', { name: 'change enterprise now' })).toHaveAttribute(
|
expect(screen.getByRole('link', { name: 'change enterprise now' })).toHaveAttribute('href', `${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=http%3A%2F%2Flocalhost%2Fcourse%2Ftest-course-id%2Fhome`);
|
||||||
'href', `${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=http%3A%2F%2Flocalhost%2Fcourse%2Ftest-course-id%2Fhome`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ export default function useActiveEnterpriseAlert(courseId) {
|
|||||||
*/
|
*/
|
||||||
const isVisible = courseAccess && !courseAccess.hasAccess && courseAccess.errorCode === 'incorrect_active_enterprise';
|
const isVisible = courseAccess && !courseAccess.hasAccess && courseAccess.errorCode === 'incorrect_active_enterprise';
|
||||||
|
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
text: courseAccess && courseAccess.userMessage,
|
text: courseAccess && courseAccess.userMessage,
|
||||||
courseId,
|
courseId,
|
||||||
};
|
}), [courseAccess, courseId]);
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientActiveEnterpriseAlert',
|
code: 'clientActiveEnterpriseAlert',
|
||||||
topic: 'outline',
|
topic: 'outline',
|
||||||
dismissible: false,
|
dismissible: false,
|
||||||
type: ALERT_TYPES.ERROR,
|
type: ALERT_TYPES.ERROR,
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { clientActiveEnterpriseAlert: ActiveEnterpriseAlert };
|
return { clientActiveEnterpriseAlert: ActiveEnterpriseAlert };
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import { Info } from '@edx/paragon/icons';
|
|||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
const DAY_MS = 24 * 60 * 60 * 1000; // in ms
|
const DAY_SEC = 24 * 60 * 60; // in seconds
|
||||||
|
const DAY_MS = DAY_SEC * 1000; // in ms
|
||||||
|
const YEAR_SEC = 365 * DAY_SEC; // in seconds
|
||||||
|
|
||||||
function CourseStartAlert({ payload }) {
|
const CourseStartAlert = ({ payload }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = payload;
|
} = payload;
|
||||||
@@ -25,15 +27,17 @@ function CourseStartAlert({ payload }) {
|
|||||||
|
|
||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
|
const delta = new Date(startDate) - new Date();
|
||||||
const timeRemaining = (
|
const timeRemaining = (
|
||||||
<FormattedRelativeTime
|
<FormattedRelativeTime
|
||||||
key="timeRemaining"
|
key="timeRemaining"
|
||||||
value={startDate}
|
value={delta / 1000}
|
||||||
|
numeric="auto"
|
||||||
|
// 1 year interval to help auto format. It won't format without updateIntervalInSeconds.
|
||||||
|
updateIntervalInSeconds={YEAR_SEC}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const delta = new Date(startDate) - new Date();
|
|
||||||
if (delta < DAY_MS) {
|
if (delta < DAY_MS) {
|
||||||
return (
|
return (
|
||||||
<Alert variant="info" icon={Info}>
|
<Alert variant="info" icon={Info}>
|
||||||
@@ -90,7 +94,7 @@ function CourseStartAlert({ payload }) {
|
|||||||
/>
|
/>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseStartAlert.propTypes = {
|
CourseStartAlert.propTypes = {
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PageBanner } from '@edx/paragon';
|
|||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
function CourseStartMasqueradeBanner({ payload }) {
|
const CourseStartMasqueradeBanner = ({ payload }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = payload;
|
} = payload;
|
||||||
@@ -33,7 +33,7 @@ function CourseStartMasqueradeBanner({ payload }) {
|
|||||||
/>
|
/>
|
||||||
</PageBanner>
|
</PageBanner>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseStartMasqueradeBanner.propTypes = {
|
CourseStartMasqueradeBanner.propTypes = {
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useModel } from '../../generic/model-store';
|
|||||||
const CourseStartAlert = React.lazy(() => import('./CourseStartAlert'));
|
const CourseStartAlert = React.lazy(() => import('./CourseStartAlert'));
|
||||||
const CourseStartMasqueradeBanner = React.lazy(() => import('./CourseStartMasqueradeBanner'));
|
const CourseStartMasqueradeBanner = React.lazy(() => import('./CourseStartMasqueradeBanner'));
|
||||||
|
|
||||||
function isStartDateInFuture(courseId) {
|
function IsStartDateInFuture(courseId) {
|
||||||
const {
|
const {
|
||||||
start,
|
start,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
@@ -20,15 +20,15 @@ function useCourseStartAlert(courseId) {
|
|||||||
isEnrolled,
|
isEnrolled,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
const isVisible = isEnrolled && isStartDateInFuture(courseId);
|
const isVisible = isEnrolled && IsStartDateInFuture(courseId);
|
||||||
|
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
courseId,
|
courseId,
|
||||||
};
|
}), [courseId]);
|
||||||
|
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientCourseStartAlert',
|
code: 'clientCourseStartAlert',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'outline-course-alerts',
|
topic: 'outline-course-alerts',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,15 +42,15 @@ export function useCourseStartMasqueradeBanner(courseId, tab) {
|
|||||||
isMasquerading,
|
isMasquerading,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
const isVisible = isMasquerading && tab === 'progress' && isStartDateInFuture(courseId);
|
const isVisible = isMasquerading && tab === 'progress' && IsStartDateInFuture(courseId);
|
||||||
|
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
courseId,
|
courseId,
|
||||||
};
|
}), [courseId]);
|
||||||
|
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientCourseStartMasqueradeBanner',
|
code: 'clientCourseStartMasqueradeBanner',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'instructor-toolbar-alerts',
|
topic: 'instructor-toolbar-alerts',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import useEnrollClickHandler from './clickHook';
|
import useEnrollClickHandler from './clickHook';
|
||||||
|
|
||||||
function EnrollmentAlert({ intl, payload }) {
|
const EnrollmentAlert = ({ intl, payload }) => {
|
||||||
const {
|
const {
|
||||||
canEnroll,
|
canEnroll,
|
||||||
courseId,
|
courseId,
|
||||||
@@ -55,7 +55,7 @@ function EnrollmentAlert({ intl, payload }) {
|
|||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
EnrollmentAlert.propTypes = {
|
EnrollmentAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function useEnrollClickHandler(courseId, orgId, successText) {
|
|||||||
});
|
});
|
||||||
global.location.reload();
|
global.location.reload();
|
||||||
});
|
});
|
||||||
}, [courseId]);
|
}, [addFlash, courseId, orgId, successText]);
|
||||||
|
|
||||||
return { enrollClickHandler, loading };
|
return { enrollClickHandler, loading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,16 +22,16 @@ export function useEnrollmentAlert(courseId) {
|
|||||||
* 3. the course is private.
|
* 3. the course is private.
|
||||||
*/
|
*/
|
||||||
const isVisible = !enrolledUser && authenticatedUser !== null && privateOutline;
|
const isVisible = !enrolledUser && authenticatedUser !== null && privateOutline;
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
||||||
courseId,
|
courseId,
|
||||||
extraText: outline && outline.enrollAlert ? outline.enrollAlert.extraText : '',
|
extraText: outline && outline.enrollAlert ? outline.enrollAlert.extraText : '',
|
||||||
isStaff: course && course.isStaff,
|
isStaff: course && course.isStaff,
|
||||||
};
|
}), [course, courseId, outline]);
|
||||||
|
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientEnrollmentAlert',
|
code: 'clientEnrollmentAlert',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'outline',
|
topic: 'outline',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
|
|||||||
import { sendActivationEmail } from '../../courseware/data';
|
import { sendActivationEmail } from '../../courseware/data';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function AccountActivationAlert({
|
const AccountActivationAlert = ({
|
||||||
intl,
|
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);
|
||||||
@@ -123,7 +123,7 @@ function AccountActivationAlert({
|
|||||||
{children()}
|
{children()}
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
AccountActivationAlert.propTypes = {
|
AccountActivationAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { WarningFilled } from '@edx/paragon/icons';
|
|||||||
|
|
||||||
import genericMessages from '../../generic/messages';
|
import genericMessages from '../../generic/messages';
|
||||||
|
|
||||||
function LogistrationAlert({ intl }) {
|
const LogistrationAlert = ({ intl }) => {
|
||||||
const signIn = (
|
const signIn = (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
style={{ textDecoration: 'underline' }}
|
style={{ textDecoration: 'underline' }}
|
||||||
@@ -41,7 +41,7 @@ function LogistrationAlert({ intl }) {
|
|||||||
/>
|
/>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
LogistrationAlert.propTypes = {
|
LogistrationAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ function useSequenceEntranceExamAlert(courseId, sequenceId, intl) {
|
|||||||
|
|
||||||
if (entranceExamPassed) {
|
if (entranceExamPassed) {
|
||||||
entranceExamText = intl.formatMessage(
|
entranceExamText = intl.formatMessage(
|
||||||
messages.entranceExamTextPassed, { entranceExamCurrentScore: entranceExamCurrentScore * 100 },
|
messages.entranceExamTextPassed,
|
||||||
|
{ entranceExamCurrentScore: entranceExamCurrentScore * 100 },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
entranceExamText = intl.formatMessage(messages.entranceExamTextNotPassing, {
|
entranceExamText = intl.formatMessage(messages.entranceExamTextNotPassing, {
|
||||||
|
|||||||
@@ -34,91 +34,89 @@ Factory.define('courseHomeMetadata')
|
|||||||
currency_symbol: '$',
|
currency_symbol: '$',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.attr(
|
.attr('tabs', ['id', 'host'], (id, host) => [
|
||||||
'tabs', ['id', 'host'], (id, host) => [
|
Factory.build(
|
||||||
Factory.build(
|
'tab',
|
||||||
'tab',
|
{
|
||||||
{
|
title: 'Course',
|
||||||
title: 'Course',
|
priority: 0,
|
||||||
priority: 0,
|
slug: 'courseware',
|
||||||
slug: 'courseware',
|
type: 'courseware',
|
||||||
type: 'courseware',
|
},
|
||||||
},
|
{
|
||||||
{
|
courseId: id,
|
||||||
courseId: id,
|
host,
|
||||||
host,
|
path: 'course/',
|
||||||
path: 'course/',
|
},
|
||||||
},
|
),
|
||||||
),
|
Factory.build(
|
||||||
Factory.build(
|
'tab',
|
||||||
'tab',
|
{
|
||||||
{
|
title: 'Discussion',
|
||||||
title: 'Discussion',
|
priority: 1,
|
||||||
priority: 1,
|
slug: 'discussion',
|
||||||
slug: 'discussion',
|
type: 'discussion',
|
||||||
type: 'discussion',
|
},
|
||||||
},
|
{
|
||||||
{
|
courseId: id,
|
||||||
courseId: id,
|
host,
|
||||||
host,
|
path: 'discussion/forum/',
|
||||||
path: 'discussion/forum/',
|
},
|
||||||
},
|
),
|
||||||
),
|
Factory.build(
|
||||||
Factory.build(
|
'tab',
|
||||||
'tab',
|
{
|
||||||
{
|
title: 'Wiki',
|
||||||
title: 'Wiki',
|
priority: 2,
|
||||||
priority: 2,
|
slug: 'wiki',
|
||||||
slug: 'wiki',
|
type: 'wiki',
|
||||||
type: 'wiki',
|
},
|
||||||
},
|
{
|
||||||
{
|
courseId: id,
|
||||||
courseId: id,
|
host,
|
||||||
host,
|
path: 'course_wiki',
|
||||||
path: 'course_wiki',
|
},
|
||||||
},
|
),
|
||||||
),
|
Factory.build(
|
||||||
Factory.build(
|
'tab',
|
||||||
'tab',
|
{
|
||||||
{
|
title: 'Progress',
|
||||||
title: 'Progress',
|
priority: 3,
|
||||||
priority: 3,
|
slug: 'progress',
|
||||||
slug: 'progress',
|
type: 'progress',
|
||||||
type: 'progress',
|
},
|
||||||
},
|
{
|
||||||
{
|
courseId: id,
|
||||||
courseId: id,
|
host,
|
||||||
host,
|
path: 'progress',
|
||||||
path: 'progress',
|
},
|
||||||
},
|
),
|
||||||
),
|
Factory.build(
|
||||||
Factory.build(
|
'tab',
|
||||||
'tab',
|
{
|
||||||
{
|
title: 'Instructor',
|
||||||
title: 'Instructor',
|
priority: 4,
|
||||||
priority: 4,
|
slug: 'instructor',
|
||||||
slug: 'instructor',
|
type: 'instructor',
|
||||||
type: 'instructor',
|
},
|
||||||
},
|
{
|
||||||
{
|
courseId: id,
|
||||||
courseId: id,
|
host,
|
||||||
host,
|
path: 'instructor',
|
||||||
path: 'instructor',
|
},
|
||||||
},
|
),
|
||||||
),
|
Factory.build(
|
||||||
Factory.build(
|
'tab',
|
||||||
'tab',
|
{
|
||||||
{
|
title: 'Dates',
|
||||||
title: 'Dates',
|
priority: 5,
|
||||||
priority: 5,
|
slug: 'dates',
|
||||||
slug: 'dates',
|
type: 'dates',
|
||||||
type: 'dates',
|
},
|
||||||
},
|
{
|
||||||
{
|
courseId: id,
|
||||||
courseId: id,
|
host,
|
||||||
host,
|
path: 'dates',
|
||||||
path: 'dates',
|
},
|
||||||
},
|
),
|
||||||
),
|
]);
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ Factory.define('outlineTabData')
|
|||||||
upgrade_url: `${host}/dashboard`,
|
upgrade_url: `${host}/dashboard`,
|
||||||
}))
|
}))
|
||||||
.attrs({
|
.attrs({
|
||||||
|
course_access_redirect: false,
|
||||||
has_scheduled_content: null,
|
has_scheduled_content: null,
|
||||||
access_expiration: null,
|
access_expiration: null,
|
||||||
can_show_upgrade_sock: false,
|
can_show_upgrade_sock: false,
|
||||||
|
|||||||
@@ -204,12 +204,18 @@ export async function getDatesTabData(courseId) {
|
|||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
return camelCaseObject(data);
|
return camelCaseObject(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { httpErrorStatus } = error && error.customAttributes;
|
const httpErrorStatus = error?.response?.status;
|
||||||
if (httpErrorStatus === 401) {
|
if (httpErrorStatus === 401) {
|
||||||
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
|
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
|
||||||
// courseAccess in the metadata call, so just ignore this status for now.
|
// courseAccess in the metadata call, so just ignore this status for now.
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
if (httpErrorStatus === 403) {
|
||||||
|
// The backend sends this if there is a course access error and the user should be redirected. The redirect
|
||||||
|
// info is included in the course metadata request and will be handled there as long as this call returns
|
||||||
|
// without an error
|
||||||
|
return {};
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +265,7 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
|
|
||||||
return camelCasedData;
|
return camelCasedData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { httpErrorStatus } = error && error.customAttributes;
|
const httpErrorStatus = error?.response?.status;
|
||||||
if (httpErrorStatus === 404) {
|
if (httpErrorStatus === 404) {
|
||||||
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/progress`);
|
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/progress`);
|
||||||
return {};
|
return {};
|
||||||
@@ -269,6 +275,12 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
// courseAccess in the metadata call, so just ignore this status for now.
|
// courseAccess in the metadata call, so just ignore this status for now.
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
if (httpErrorStatus === 403) {
|
||||||
|
// The backend sends this if there is a course access error and the user should be redirected. The redirect
|
||||||
|
// info is included in the course metadata request and will be handled there as long as this call returns
|
||||||
|
// without an error
|
||||||
|
return {};
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +334,20 @@ export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
|
|||||||
export async function getOutlineTabData(courseId) {
|
export async function getOutlineTabData(courseId) {
|
||||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
const url = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
||||||
const requestTime = Date.now();
|
const requestTime = Date.now();
|
||||||
const tabData = await getAuthenticatedHttpClient().get(url);
|
let tabData;
|
||||||
|
try {
|
||||||
|
tabData = await getAuthenticatedHttpClient().get(url);
|
||||||
|
} catch (error) {
|
||||||
|
const httpErrorStatus = error?.response?.status;
|
||||||
|
if (httpErrorStatus === 403) {
|
||||||
|
// The backend sends this if there is a course access error and the user should be redirected. The redirect
|
||||||
|
// info is included in the course metadata request and will be handled there as long as this call returns
|
||||||
|
// without an error
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const responseTime = Date.now();
|
const responseTime = Date.now();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -349,7 +374,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
const timeOffsetMillis = getTimeOffsetMillis(headers && headers.date, requestTime, responseTime);
|
const timeOffsetMillis = getTimeOffsetMillis(headers && headers.date, requestTime, responseTime);
|
||||||
const userHasPassingGrade = data.user_has_passing_grade;
|
const userHasPassingGrade = data.user_has_passing_grade;
|
||||||
const verifiedMode = camelCaseObject(data.verified_mode);
|
const verifiedMode = camelCaseObject(data.verified_mode);
|
||||||
const welcomeMessageHtml = data.welcome_message_html;
|
const welcomeMessageHtml = data.welcome_message_html || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Pact, Matchers } from '@pact-foundation/pact';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { mergeConfig, getConfig } from '@edx/frontend-platform';
|
import { mergeConfig, getConfig } from '@edx/frontend-platform';
|
||||||
|
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCourseHomeCourseMetadata,
|
getCourseHomeCourseMetadata,
|
||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
somethingLike: like, term, boolean, string, eachLike,
|
somethingLike: like, term, boolean, string, eachLike,
|
||||||
} = Matchers;
|
} = MatchersV3;
|
||||||
const provider = new Pact({
|
const provider = new PactV3({
|
||||||
consumer: 'frontend-app-learning',
|
consumer: 'frontend-app-learning',
|
||||||
provider: 'lms',
|
provider: 'lms',
|
||||||
log: path.resolve(process.cwd(), 'src/course-home/data/pact-tests/logs', 'pact.log'),
|
log: path.resolve(process.cwd(), 'src/course-home/data/pact-tests/logs', 'pact.log'),
|
||||||
@@ -28,194 +28,193 @@ const provider = new Pact({
|
|||||||
describe('Course Home Service', () => {
|
describe('Course Home Service', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
initializeMockApp();
|
initializeMockApp();
|
||||||
await provider
|
mergeConfig({
|
||||||
.setup()
|
LMS_BASE_URL: 'http://localhost:8081',
|
||||||
.then((options) => mergeConfig({
|
}, 'Custom app config for pact tests');
|
||||||
LMS_BASE_URL: `http://localhost:${options.port}`,
|
|
||||||
}, 'Custom app config for pact tests'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => provider.verify());
|
|
||||||
afterAll(() => provider.finalize());
|
|
||||||
describe('When a request to fetch tab is made', () => {
|
describe('When a request to fetch tab is made', () => {
|
||||||
it('returns tab data for a course_id', async () => {
|
it('returns tab data for a course_id', async () => {
|
||||||
await provider.addInteraction({
|
setTimeout(() => {
|
||||||
state: `Tab data exists for course_id ${courseId}`,
|
provider.addInteraction({
|
||||||
uponReceiving: 'a request to fetch tab',
|
state: `Tab data exists for course_id ${courseId}`,
|
||||||
withRequest: {
|
uponReceiving: 'a request to fetch tab',
|
||||||
method: 'GET',
|
withRequest: {
|
||||||
path: `/api/course_home/course_metadata/${courseId}`,
|
method: 'GET',
|
||||||
},
|
path: `/api/course_home/course_metadata/${courseId}`,
|
||||||
willRespondWith: {
|
},
|
||||||
status: 200,
|
willRespondWith: {
|
||||||
body: {
|
status: 200,
|
||||||
can_show_upgrade_sock: boolean(false),
|
body: {
|
||||||
verified_mode: like({
|
can_show_upgrade_sock: boolean(false),
|
||||||
access_expiration_date: null,
|
verified_mode: like({
|
||||||
currency: 'USD',
|
access_expiration_date: null,
|
||||||
currency_symbol: '$',
|
currency: 'USD',
|
||||||
price: 149,
|
currency_symbol: '$',
|
||||||
sku: '8CF08E5',
|
price: 149,
|
||||||
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
sku: '8CF08E5',
|
||||||
}),
|
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
celebrations: like({
|
}),
|
||||||
first_section: false,
|
celebrations: like({
|
||||||
streak_length_to_celebrate: null,
|
first_section: false,
|
||||||
streak_discount_enabled: false,
|
streak_length_to_celebrate: null,
|
||||||
}),
|
streak_discount_enabled: false,
|
||||||
course_access: {
|
}),
|
||||||
has_access: boolean(true),
|
course_access: {
|
||||||
error_code: null,
|
has_access: boolean(true),
|
||||||
developer_message: null,
|
error_code: null,
|
||||||
user_message: null,
|
developer_message: null,
|
||||||
additional_context_user_message: null,
|
user_message: null,
|
||||||
user_fragment: null,
|
additional_context_user_message: null,
|
||||||
|
user_fragment: null,
|
||||||
|
},
|
||||||
|
course_id: term({
|
||||||
|
generate: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
matcher: opaqueKeysRegex,
|
||||||
|
}),
|
||||||
|
is_enrolled: boolean(true),
|
||||||
|
is_self_paced: boolean(false),
|
||||||
|
is_staff: boolean(true),
|
||||||
|
number: string('DemoX'),
|
||||||
|
org: string('edX'),
|
||||||
|
original_user_is_staff: boolean(true),
|
||||||
|
start: term({
|
||||||
|
generate: '2013-02-05T05:00:00Z',
|
||||||
|
matcher: dateRegex,
|
||||||
|
}),
|
||||||
|
tabs: eachLike({
|
||||||
|
tab_id: 'courseware',
|
||||||
|
title: 'Course',
|
||||||
|
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||||
|
}),
|
||||||
|
title: string('Demonstration Course'),
|
||||||
|
username: string('edx'),
|
||||||
},
|
},
|
||||||
course_id: term({
|
},
|
||||||
generate: 'course-v1:edX+DemoX+Demo_Course',
|
});
|
||||||
matcher: opaqueKeysRegex,
|
const normalizedTabData = {
|
||||||
}),
|
canShowUpgradeSock: false,
|
||||||
is_enrolled: boolean(true),
|
verifiedMode: {
|
||||||
is_self_paced: boolean(false),
|
accessExpirationDate: null,
|
||||||
is_staff: boolean(true),
|
currency: 'USD',
|
||||||
number: string('DemoX'),
|
currencySymbol: '$',
|
||||||
org: string('edX'),
|
price: 149,
|
||||||
original_user_is_staff: boolean(true),
|
sku: '8CF08E5',
|
||||||
start: term({
|
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
generate: '2013-02-05T05:00:00Z',
|
},
|
||||||
matcher: dateRegex,
|
celebrations: {
|
||||||
}),
|
firstSection: false,
|
||||||
tabs: eachLike({
|
streakLengthToCelebrate: null,
|
||||||
tab_id: 'courseware',
|
streakDiscountEnabled: false,
|
||||||
|
},
|
||||||
|
courseAccess: {
|
||||||
|
hasAccess: true,
|
||||||
|
errorCode: null,
|
||||||
|
developerMessage: null,
|
||||||
|
userMessage: null,
|
||||||
|
additionalContextUserMessage: null,
|
||||||
|
userFragment: null,
|
||||||
|
},
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
isEnrolled: true,
|
||||||
|
isMasquerading: false,
|
||||||
|
isSelfPaced: false,
|
||||||
|
isStaff: true,
|
||||||
|
number: 'DemoX',
|
||||||
|
org: 'edX',
|
||||||
|
originalUserIsStaff: true,
|
||||||
|
start: '2013-02-05T05:00:00Z',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
slug: 'outline',
|
||||||
title: 'Course',
|
title: 'Course',
|
||||||
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||||
}),
|
},
|
||||||
title: string('Demonstration Course'),
|
],
|
||||||
username: string('edx'),
|
title: 'Demonstration Course',
|
||||||
},
|
username: 'edx',
|
||||||
},
|
};
|
||||||
});
|
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
||||||
const normalizedTabData = {
|
expect(response).toBeTruthy();
|
||||||
canShowUpgradeSock: false,
|
expect(response).toEqual(normalizedTabData);
|
||||||
verifiedMode: {
|
}, 100);
|
||||||
accessExpirationDate: null,
|
|
||||||
currency: 'USD',
|
|
||||||
currencySymbol: '$',
|
|
||||||
price: 149,
|
|
||||||
sku: '8CF08E5',
|
|
||||||
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
|
||||||
},
|
|
||||||
celebrations: {
|
|
||||||
firstSection: false,
|
|
||||||
streakLengthToCelebrate: null,
|
|
||||||
streakDiscountEnabled: false,
|
|
||||||
},
|
|
||||||
courseAccess: {
|
|
||||||
hasAccess: true,
|
|
||||||
errorCode: null,
|
|
||||||
developerMessage: null,
|
|
||||||
userMessage: null,
|
|
||||||
additionalContextUserMessage: null,
|
|
||||||
userFragment: null,
|
|
||||||
},
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
isEnrolled: true,
|
|
||||||
isMasquerading: false,
|
|
||||||
isSelfPaced: false,
|
|
||||||
isStaff: true,
|
|
||||||
number: 'DemoX',
|
|
||||||
org: 'edX',
|
|
||||||
originalUserIsStaff: true,
|
|
||||||
start: '2013-02-05T05:00:00Z',
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
slug: 'outline',
|
|
||||||
title: 'Course',
|
|
||||||
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: 'Demonstration Course',
|
|
||||||
username: 'edx',
|
|
||||||
};
|
|
||||||
const response = await getCourseHomeCourseMetadata(courseId, 'outline');
|
|
||||||
expect(response).toBeTruthy();
|
|
||||||
expect(response).toEqual(normalizedTabData);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When a request to fetch dates tab is made', () => {
|
describe('When a request to fetch dates tab is made', () => {
|
||||||
it('returns course date blocks for a course_id', async () => {
|
it('returns course date blocks for a course_id', async () => {
|
||||||
await provider.addInteraction({
|
setTimeout(() => {
|
||||||
state: `course date blocks exist for course_id ${courseId}`,
|
provider.addInteraction({
|
||||||
uponReceiving: 'a request to fetch dates tab',
|
state: `course date blocks exist for course_id ${courseId}`,
|
||||||
withRequest: {
|
uponReceiving: 'a request to fetch dates tab',
|
||||||
method: 'GET',
|
withRequest: {
|
||||||
path: `/api/course_home/dates/${courseId}`,
|
method: 'GET',
|
||||||
},
|
path: `/api/course_home/dates/${courseId}`,
|
||||||
willRespondWith: {
|
},
|
||||||
status: 200,
|
willRespondWith: {
|
||||||
body: {
|
status: 200,
|
||||||
dates_banner_info: like({
|
body: {
|
||||||
missed_deadlines: false,
|
dates_banner_info: like({
|
||||||
content_type_gating_enabled: false,
|
missed_deadlines: false,
|
||||||
missed_gated_content: false,
|
content_type_gating_enabled: false,
|
||||||
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
missed_gated_content: false,
|
||||||
}),
|
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
course_date_blocks: eachLike({
|
}),
|
||||||
assignment_type: null,
|
course_date_blocks: eachLike({
|
||||||
|
assignment_type: null,
|
||||||
|
complete: null,
|
||||||
|
date: term({
|
||||||
|
generate: '2013-02-05T05:00:00Z',
|
||||||
|
matcher: dateRegex,
|
||||||
|
}),
|
||||||
|
date_type: term({
|
||||||
|
generate: 'verified-upgrade-deadline',
|
||||||
|
matcher: dateTypeRegex,
|
||||||
|
}),
|
||||||
|
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
|
||||||
|
learner_has_access: true,
|
||||||
|
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
|
link_text: 'Upgrade to Verified Certificate',
|
||||||
|
title: 'Verification Upgrade Deadline',
|
||||||
|
extra_info: null,
|
||||||
|
first_component_block_id: '',
|
||||||
|
}),
|
||||||
|
has_ended: boolean(false),
|
||||||
|
learner_is_full_access: boolean(true),
|
||||||
|
user_timezone: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const camelCaseResponse = {
|
||||||
|
datesBannerInfo: {
|
||||||
|
missedDeadlines: false,
|
||||||
|
contentTypeGatingEnabled: false,
|
||||||
|
missedGatedContent: false,
|
||||||
|
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
|
},
|
||||||
|
courseDateBlocks: [
|
||||||
|
{
|
||||||
|
assignmentType: null,
|
||||||
complete: null,
|
complete: null,
|
||||||
date: term({
|
date: '2013-02-05T05:00:00Z',
|
||||||
generate: '2013-02-05T05:00:00Z',
|
dateType: 'verified-upgrade-deadline',
|
||||||
matcher: dateRegex,
|
|
||||||
}),
|
|
||||||
date_type: term({
|
|
||||||
generate: 'verified-upgrade-deadline',
|
|
||||||
matcher: dateTypeRegex,
|
|
||||||
}),
|
|
||||||
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
|
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
|
||||||
learner_has_access: true,
|
learnerHasAccess: true,
|
||||||
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
link_text: 'Upgrade to Verified Certificate',
|
linkText: 'Upgrade to Verified Certificate',
|
||||||
title: 'Verification Upgrade Deadline',
|
title: 'Verification Upgrade Deadline',
|
||||||
extra_info: null,
|
extraInfo: null,
|
||||||
first_component_block_id: '',
|
firstComponentBlockId: '',
|
||||||
}),
|
},
|
||||||
has_ended: boolean(false),
|
],
|
||||||
learner_is_full_access: boolean(true),
|
hasEnded: false,
|
||||||
user_timezone: null,
|
learnerIsFullAccess: true,
|
||||||
},
|
userTimezone: null,
|
||||||
},
|
};
|
||||||
});
|
const response = getDatesTabData(courseId);
|
||||||
const camelCaseResponse = {
|
expect(response).toBeTruthy();
|
||||||
datesBannerInfo: {
|
expect(response).toEqual(camelCaseResponse);
|
||||||
missedDeadlines: false,
|
}, 100);
|
||||||
contentTypeGatingEnabled: false,
|
|
||||||
missedGatedContent: false,
|
|
||||||
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
|
||||||
},
|
|
||||||
courseDateBlocks: [
|
|
||||||
{
|
|
||||||
assignmentType: null,
|
|
||||||
complete: null,
|
|
||||||
date: '2013-02-05T05:00:00Z',
|
|
||||||
dateType: 'verified-upgrade-deadline',
|
|
||||||
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
|
|
||||||
learnerHasAccess: true,
|
|
||||||
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
|
||||||
linkText: 'Upgrade to Verified Certificate',
|
|
||||||
title: 'Verification Upgrade Deadline',
|
|
||||||
extraInfo: null,
|
|
||||||
firstComponentBlockId: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hasEnded: false,
|
|
||||||
learnerIsFullAccess: true,
|
|
||||||
userTimezone: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await getDatesTabData(courseId);
|
|
||||||
expect(response).toBeTruthy();
|
|
||||||
expect(response).toEqual(camelCaseResponse);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ describe('Data layer integration tests', () => {
|
|||||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
||||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||||
|
|
||||||
|
const courseHomeAccessDeniedMetadata = Factory.build(
|
||||||
|
'courseHomeMetadata',
|
||||||
|
{
|
||||||
|
id: courseId,
|
||||||
|
course_access: {
|
||||||
|
has_access: false,
|
||||||
|
error_code: 'bad codes',
|
||||||
|
additional_context_user_message: 'your Codes Are BAD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -57,14 +69,31 @@ describe('Data layer integration tests', () => {
|
|||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toMatchSnapshot();
|
expect(state).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([401, 403, 404])(
|
||||||
|
'should result in fetch denied for expected errors and failed for all others',
|
||||||
|
async (errorStatus) => {
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
|
||||||
|
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).reply(errorStatus, {});
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
|
||||||
|
|
||||||
|
let expectedState = 'failed';
|
||||||
|
if (errorStatus === 401 || errorStatus === 403) {
|
||||||
|
expectedState = 'denied';
|
||||||
|
}
|
||||||
|
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test fetchOutlineTab', () => {
|
describe('Test fetchOutlineTab', () => {
|
||||||
const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline`;
|
const outlineBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline`;
|
||||||
|
const outlineUrl = `${outlineBaseUrl}/${courseId}`;
|
||||||
|
|
||||||
it('Should result in fetch failure if error occurs', async () => {
|
it('Should result in fetch failure if error occurs', async () => {
|
||||||
axiosMock.onGet(courseMetadataUrl).networkError();
|
axiosMock.onGet(courseMetadataUrl).networkError();
|
||||||
axiosMock.onGet(`${outlineBaseUrl}/${courseId}`).networkError();
|
axiosMock.onGet(outlineUrl).networkError();
|
||||||
|
|
||||||
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
|
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
|
||||||
|
|
||||||
@@ -75,8 +104,6 @@ describe('Data layer integration tests', () => {
|
|||||||
it('Should fetch, normalize, and save metadata', async () => {
|
it('Should fetch, normalize, and save metadata', async () => {
|
||||||
const outlineTabData = Factory.build('outlineTabData', { courseId });
|
const outlineTabData = Factory.build('outlineTabData', { courseId });
|
||||||
|
|
||||||
const outlineUrl = `${outlineBaseUrl}/${courseId}`;
|
|
||||||
|
|
||||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
|
||||||
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
|
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
|
||||||
|
|
||||||
@@ -86,6 +113,22 @@ describe('Data layer integration tests', () => {
|
|||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toMatchSnapshot();
|
expect(state).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([401, 403, 404])(
|
||||||
|
'should result in fetch denied for expected errors and failed for all others',
|
||||||
|
async (errorStatus) => {
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
|
||||||
|
axiosMock.onGet(outlineUrl).reply(errorStatus, {});
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
|
||||||
|
|
||||||
|
let expectedState = 'failed';
|
||||||
|
if (errorStatus === 403) {
|
||||||
|
expectedState = 'denied';
|
||||||
|
}
|
||||||
|
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test fetchProgressTab', () => {
|
describe('Test fetchProgressTab', () => {
|
||||||
@@ -129,6 +172,19 @@ describe('Data layer integration tests', () => {
|
|||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.courseHome.targetUserId).toEqual(2);
|
expect(state.courseHome.targetUserId).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([401, 403, 404])(
|
||||||
|
'should result in fetch denied for expected errors and failed for all others',
|
||||||
|
async (errorStatus) => {
|
||||||
|
const progressUrl = `${progressBaseUrl}/${courseId}`;
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
|
||||||
|
axiosMock.onGet(progressUrl).reply(errorStatus, {});
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||||
|
|
||||||
|
expect(store.getState().courseHome.courseStatus).toEqual('denied');
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test saveCourseGoal', () => {
|
describe('Test saveCourseGoal', () => {
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import Timeline from './timeline/Timeline';
|
|||||||
import { fetchDatesTab } from '../data';
|
import { fetchDatesTab } from '../data';
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
import { initDatesMMP2P } from '../../experiments/mm-p2p';
|
|
||||||
import SuggestedScheduleHeader from '../suggested-schedule-messaging/SuggestedScheduleHeader';
|
import SuggestedScheduleHeader from '../suggested-schedule-messaging/SuggestedScheduleHeader';
|
||||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
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';
|
||||||
|
|
||||||
function DatesTab({ intl }) {
|
const DatesTab = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -30,9 +28,6 @@ function DatesTab({ intl }) {
|
|||||||
courseDateBlocks,
|
courseDateBlocks,
|
||||||
} = useModel('dates', courseId);
|
} = useModel('dates', courseId);
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
const mmp2p = initDatesMMP2P(courseId);
|
|
||||||
|
|
||||||
const hasDeadlines = courseDateBlocks && courseDateBlocks.some(x => x.dateType === 'assignment-due-date');
|
const hasDeadlines = courseDateBlocks && courseDateBlocks.some(x => x.dateType === 'assignment-due-date');
|
||||||
|
|
||||||
const logUpgradeLinkClick = () => {
|
const logUpgradeLinkClick = () => {
|
||||||
@@ -51,8 +46,7 @@ function DatesTab({ intl }) {
|
|||||||
<div role="heading" aria-level="1" className="h2 my-3">
|
<div role="heading" aria-level="1" className="h2 my-3">
|
||||||
{intl.formatMessage(messages.title)}
|
{intl.formatMessage(messages.title)}
|
||||||
</div>
|
</div>
|
||||||
{ /** [MM-P2P] Experiment */ }
|
{isSelfPaced && hasDeadlines && (
|
||||||
{isSelfPaced && hasDeadlines && !mmp2p.state.isEnabled && (
|
|
||||||
<>
|
<>
|
||||||
<ShiftDatesAlert model="dates" fetch={fetchDatesTab} />
|
<ShiftDatesAlert model="dates" fetch={fetchDatesTab} />
|
||||||
<SuggestedScheduleHeader />
|
<SuggestedScheduleHeader />
|
||||||
@@ -60,10 +54,10 @@ function DatesTab({ intl }) {
|
|||||||
<UpgradeToShiftDatesAlert logUpgradeLinkClick={logUpgradeLinkClick} model="dates" />
|
<UpgradeToShiftDatesAlert logUpgradeLinkClick={logUpgradeLinkClick} model="dates" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Timeline mmp2p={mmp2p} />
|
<Timeline />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
DatesTab.propTypes = {
|
DatesTab.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -17,15 +17,13 @@ import { useModel } from '../../../generic/model-store';
|
|||||||
import { getBadgeListAndColor } from './badgelist';
|
import { getBadgeListAndColor } from './badgelist';
|
||||||
import { isLearnerAssignment } from '../utils';
|
import { isLearnerAssignment } from '../utils';
|
||||||
|
|
||||||
function Day({
|
const Day = ({
|
||||||
date,
|
date,
|
||||||
first,
|
first,
|
||||||
intl,
|
intl,
|
||||||
items,
|
items,
|
||||||
last,
|
last,
|
||||||
/** [MM-P2P] Example */
|
}) => {
|
||||||
mmp2p,
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -37,11 +35,6 @@ function Day({
|
|||||||
|
|
||||||
const { color, badges } = getBadgeListAndColor(date, intl, null, items);
|
const { color, badges } = getBadgeListAndColor(date, intl, null, items);
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
const mmp2pOverride = (
|
|
||||||
mmp2p.state.isEnabled
|
|
||||||
&& items.some((item) => item.dateType === 'verified-upgrade-deadline')
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<li className="dates-day pb-4" data-testid="dates-day">
|
<li className="dates-day pb-4" data-testid="dates-day">
|
||||||
{/* Top Line */}
|
{/* Top Line */}
|
||||||
@@ -57,8 +50,7 @@ function Day({
|
|||||||
<div className="d-inline-block ml-3 pl-2">
|
<div className="d-inline-block ml-3 pl-2">
|
||||||
<div className="row w-100 m-0 mb-1 align-items-center text-primary-700" data-testid="dates-header">
|
<div className="row w-100 m-0 mb-1 align-items-center text-primary-700" data-testid="dates-header">
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
/** [MM-P2P] Experiment */
|
value={date}
|
||||||
value={mmp2pOverride ? mmp2p.state.upgradeDeadline : date}
|
|
||||||
day="numeric"
|
day="numeric"
|
||||||
month="short"
|
month="short"
|
||||||
weekday="short"
|
weekday="short"
|
||||||
@@ -68,10 +60,7 @@ function Day({
|
|||||||
{badges}
|
{badges}
|
||||||
</div>
|
</div>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
/** [MM-P2P] Experiment (conditional) */
|
const { badges: itemBadges } = getBadgeListAndColor(date, intl, item, items);
|
||||||
const { badges: itemBadges } = mmp2pOverride
|
|
||||||
? getBadgeListAndColor(new Date(mmp2p.state.upgradeDeadline), intl, item, items)
|
|
||||||
: getBadgeListAndColor(date, intl, item, items);
|
|
||||||
|
|
||||||
const showDueDateTime = item.dateType === 'assignment-due-date';
|
const showDueDateTime = item.dateType === 'assignment-due-date';
|
||||||
const showLink = item.link && isLearnerAssignment(item);
|
const showLink = item.link && isLearnerAssignment(item);
|
||||||
@@ -107,22 +96,14 @@ function Day({
|
|||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{ /** [MM-P2P] Experiment (conditional) */ }
|
{item.description && <div className="small mb-2">{item.description}</div>}
|
||||||
{ mmp2pOverride
|
|
||||||
? (
|
|
||||||
<div className="small mb-2">
|
|
||||||
You are still eligible to upgrade to a Verified Certificate!
|
|
||||||
Unlock full course access and highlight the knowledge you'll gain.
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (item.description && <div className="small mb-2">{item.description}</div>)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Day.propTypes = {
|
Day.propTypes = {
|
||||||
date: PropTypes.objectOf(Date).isRequired,
|
date: PropTypes.objectOf(Date).isRequired,
|
||||||
@@ -138,25 +119,11 @@ Day.propTypes = {
|
|||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
last: PropTypes.bool,
|
last: PropTypes.bool,
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: PropTypes.shape({
|
|
||||||
state: PropTypes.shape({
|
|
||||||
isEnabled: PropTypes.bool.isRequired,
|
|
||||||
upgradeDeadline: PropTypes.string,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Day.defaultProps = {
|
Day.defaultProps = {
|
||||||
first: false,
|
first: false,
|
||||||
last: false,
|
last: false,
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: {
|
|
||||||
state: {
|
|
||||||
isEnabled: false,
|
|
||||||
upgradeDeadline: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(Day);
|
export default injectIntl(Day);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
/** [MM-P2P] Experiment (import) */
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
@@ -8,8 +6,7 @@ import { useModel } from '../../../generic/model-store';
|
|||||||
import Day from './Day';
|
import Day from './Day';
|
||||||
import { daycmp, isLearnerAssignment } from '../utils';
|
import { daycmp, isLearnerAssignment } from '../utils';
|
||||||
|
|
||||||
/** [MM-P2P] Experiment (argument) */
|
const Timeline = () => {
|
||||||
export default function Timeline({ mmp2p }) {
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -66,17 +63,10 @@ export default function Timeline({ mmp2p }) {
|
|||||||
return (
|
return (
|
||||||
<ul className="list-unstyled m-0 mt-4 pt-2">
|
<ul className="list-unstyled m-0 mt-4 pt-2">
|
||||||
{groupedDates.map((groupedDate) => (
|
{groupedDates.map((groupedDate) => (
|
||||||
<Day key={groupedDate.date} {...groupedDate} mmp2p={mmp2p} />
|
<Day key={groupedDate.date} {...groupedDate} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
Timeline.propTypes = {
|
|
||||||
mmp2p: PropTypes.shape({}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Timeline.defaultProps = {
|
export default Timeline;
|
||||||
mmp2p: {},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { generatePath, useHistory } from 'react-router';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks';
|
import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks';
|
||||||
|
|
||||||
function DiscussionTab() {
|
const DiscussionTab = () => {
|
||||||
const { courseId } = useSelector(state => state.courseHome);
|
const { courseId } = useSelector(state => state.courseHome);
|
||||||
const { path } = useParams();
|
const { path } = useParams();
|
||||||
const [originalPath] = useState(path);
|
const [originalPath] = useState(path);
|
||||||
@@ -29,7 +29,7 @@ function DiscussionTab() {
|
|||||||
title="discussion"
|
title="discussion"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
DiscussionTab.propTypes = {};
|
DiscussionTab.propTypes = {};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { unsubscribeFromCourseGoal } from '../data/api';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ResultPage from './ResultPage';
|
import ResultPage from './ResultPage';
|
||||||
|
|
||||||
function GoalUnsubscribe({ intl }) {
|
const GoalUnsubscribe = ({ intl }) => {
|
||||||
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);
|
||||||
@@ -33,6 +33,7 @@ function GoalUnsubscribe({ intl }) {
|
|||||||
// as visiting this page is allowed to be done anonymously and without the context of the course.
|
// as visiting this page is allowed to be done anonymously and without the context of the course.
|
||||||
// The token can be used to connect a user and course, it will just require some post-processing
|
// The token can be used to connect a user and course, it will just require some post-processing
|
||||||
sendTrackEvent('edx.ui.lms.goal.unsubscribe', { token });
|
sendTrackEvent('edx.ui.lms.goal.unsubscribe', { token });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []); // deps=[] to only run once
|
}, []); // deps=[] to only run once
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +49,7 @@ function GoalUnsubscribe({ intl }) {
|
|||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
GoalUnsubscribe.propTypes = {
|
GoalUnsubscribe.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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';
|
||||||
|
|
||||||
function ResultPage({ courseTitle, error, intl }) {
|
const ResultPage = ({ courseTitle, error, intl }) => {
|
||||||
const errorDescription = (
|
const errorDescription = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="learning.goals.unsubscribe.errorDescription"
|
id="learning.goals.unsubscribe.errorDescription"
|
||||||
@@ -44,7 +44,7 @@ function ResultPage({ courseTitle, error, intl }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ResultPage.defaultProps = {
|
ResultPage.defaultProps = {
|
||||||
courseTitle: null,
|
courseTitle: null,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
function LiveTab() {
|
const LiveTab = () => {
|
||||||
const { courseId } = useSelector(state => state.courseHome);
|
const { courseId } = useSelector(state => state.courseHome);
|
||||||
const liveModel = useSelector(state => state.models.live);
|
const liveModel = useSelector(state => state.models.live);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -17,6 +17,6 @@ function LiveTab() {
|
|||||||
dangerouslySetInnerHTML={{ __html: liveModel[courseId]?.iframe }}
|
dangerouslySetInnerHTML={{ __html: liveModel[courseId]?.iframe }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LiveTab;
|
export default LiveTab;
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import { isLearnerAssignment } from '../dates-tab/utils';
|
import { isLearnerAssignment } from '../dates-tab/utils';
|
||||||
import './DateSummary.scss';
|
import './DateSummary.scss';
|
||||||
|
|
||||||
export default function DateSummary({
|
const DateSummary = ({
|
||||||
dateBlock,
|
dateBlock,
|
||||||
userTimezone,
|
userTimezone,
|
||||||
/** [MM-P2P] Experiment */
|
}) => {
|
||||||
mmp2p,
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -25,9 +23,6 @@ export default function DateSummary({
|
|||||||
const linkedTitle = dateBlock.link && isLearnerAssignment(dateBlock);
|
const linkedTitle = dateBlock.link && isLearnerAssignment(dateBlock);
|
||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
const showMMP2P = mmp2p.state.isEnabled && (dateBlock.dateType === 'verified-upgrade-deadline');
|
|
||||||
|
|
||||||
const logVerifiedUpgradeClick = () => {
|
const logVerifiedUpgradeClick = () => {
|
||||||
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
|
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
|
||||||
org_key: org,
|
org_key: org,
|
||||||
@@ -45,8 +40,7 @@ export default function DateSummary({
|
|||||||
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
|
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
|
||||||
<div className="ml-1 font-weight-bold">
|
<div className="ml-1 font-weight-bold">
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
/** [MM-P2P] Experiment */
|
value={dateBlock.date}
|
||||||
value={showMMP2P ? mmp2p.state.upgradeDeadline : dateBlock.date}
|
|
||||||
day="numeric"
|
day="numeric"
|
||||||
month="short"
|
month="short"
|
||||||
weekday="short"
|
weekday="short"
|
||||||
@@ -55,48 +49,33 @@ export default function DateSummary({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/** [MM-P2P] Experiment (conditional) */}
|
<div className="row ml-4 pr-2">
|
||||||
{ showMMP2P ? (
|
<div className="date-summary-text">
|
||||||
<div className="row ml-4 pr-2">
|
{linkedTitle && (
|
||||||
<div className="date-summary-text">
|
|
||||||
<div className="font-weight-bold mt-2">
|
<div className="font-weight-bold mt-2">
|
||||||
Last chance to upgrade
|
<a href={dateBlock.link}>{dateBlock.title}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="date-summary-text mt-1">
|
|
||||||
You are still eligible to upgrade to a Verified Certificate!
|
|
||||||
Unlock full course access and highlight the knowledge you'll gain.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="row ml-4 pr-2">
|
|
||||||
<div className="date-summary-text">
|
|
||||||
{linkedTitle && (
|
|
||||||
<div className="font-weight-bold mt-2">
|
|
||||||
<a href={dateBlock.link}>{dateBlock.title}</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!linkedTitle && (
|
|
||||||
<div className="font-weight-bold mt-2">{dateBlock.title}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{dateBlock.description && (
|
|
||||||
<div className="date-summary-text mt-1">{dateBlock.description}</div>
|
|
||||||
)}
|
)}
|
||||||
{!linkedTitle && dateBlock.link && (
|
{!linkedTitle && (
|
||||||
<a
|
<div className="font-weight-bold mt-2">{dateBlock.title}</div>
|
||||||
href={dateBlock.link}
|
|
||||||
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
|
||||||
className="description-link"
|
|
||||||
>
|
|
||||||
{dateBlock.linkText}
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{dateBlock.description && (
|
||||||
|
<div className="date-summary-text mt-1">{dateBlock.description}</div>
|
||||||
|
)}
|
||||||
|
{!linkedTitle && dateBlock.link && (
|
||||||
|
<a
|
||||||
|
href={dateBlock.link}
|
||||||
|
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
||||||
|
className="description-link"
|
||||||
|
>
|
||||||
|
{dateBlock.linkText}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
DateSummary.propTypes = {
|
DateSummary.propTypes = {
|
||||||
dateBlock: PropTypes.shape({
|
dateBlock: PropTypes.shape({
|
||||||
@@ -109,22 +88,10 @@ DateSummary.propTypes = {
|
|||||||
learnerHasAccess: PropTypes.bool,
|
learnerHasAccess: PropTypes.bool,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
userTimezone: PropTypes.string,
|
userTimezone: PropTypes.string,
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: PropTypes.shape({
|
|
||||||
state: PropTypes.shape({
|
|
||||||
isEnabled: PropTypes.bool.isRequired,
|
|
||||||
upgradeDeadline: PropTypes.string,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DateSummary.defaultProps = {
|
DateSummary.defaultProps = {
|
||||||
userTimezone: null,
|
userTimezone: null,
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: {
|
|
||||||
state: {
|
|
||||||
isEnabled: false,
|
|
||||||
upgradeDeadline: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default DateSummary;
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
export default function LmsHtmlFragment({
|
const LmsHtmlFragment = ({
|
||||||
className,
|
className,
|
||||||
html,
|
html,
|
||||||
title,
|
title,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) => {
|
||||||
const wholePage = `
|
const wholePage = `
|
||||||
<html>
|
<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">
|
||||||
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/static/LmsHtmlFragment.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="${className}">${html}</body>
|
<body class="${className}">${html}</body>
|
||||||
<script>
|
<script>
|
||||||
@@ -55,7 +55,7 @@ export default function LmsHtmlFragment({
|
|||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
LmsHtmlFragment.defaultProps = {
|
LmsHtmlFragment.defaultProps = {
|
||||||
className: '',
|
className: '',
|
||||||
@@ -66,3 +66,5 @@ LmsHtmlFragment.propTypes = {
|
|||||||
html: PropTypes.string.isRequired,
|
html: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default LmsHtmlFragment;
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ 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';
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
const OutlineTab = ({ intl }) => {
|
||||||
import { initHomeMMP2P, MMP2PFlyover } from '../../experiments/mm-p2p';
|
|
||||||
|
|
||||||
function OutlineTab({ intl }) {
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
proctoringPanelStatus,
|
proctoringPanelStatus,
|
||||||
@@ -104,9 +101,6 @@ function OutlineTab({ intl }) {
|
|||||||
return userRoleNames.includes('enterprise_learner');
|
return userRoleNames.includes('enterprise_learner');
|
||||||
};
|
};
|
||||||
|
|
||||||
/** [[MM-P2P] Experiment */
|
|
||||||
const MMP2P = initHomeMMP2P(courseId);
|
|
||||||
|
|
||||||
/** show post enrolment survey to only B2C learners */
|
/** show post enrolment survey to only B2C learners */
|
||||||
const learnerType = isEnterpriseUser() ? 'enterprise_learner' : 'b2c_learner';
|
const learnerType = isEnterpriseUser() ? 'enterprise_learner' : 'b2c_learner';
|
||||||
|
|
||||||
@@ -116,7 +110,7 @@ function OutlineTab({ intl }) {
|
|||||||
const currentParams = new URLSearchParams(location.search);
|
const currentParams = new URLSearchParams(location.search);
|
||||||
const startCourse = currentParams.get('start_course');
|
const startCourse = currentParams.get('start_course');
|
||||||
if (startCourse === '1') {
|
if (startCourse === '1') {
|
||||||
sendTrackEvent('welcome.email.clicked.startcourse', {});
|
sendTrackEvent('enrollment.email.clicked.startcourse', {});
|
||||||
|
|
||||||
// Deleting the course_start query param as it only needs to be set once
|
// Deleting the course_start query param as it only needs to be set once
|
||||||
// whenever passed in query params.
|
// whenever passed in query params.
|
||||||
@@ -134,7 +128,6 @@ function OutlineTab({ intl }) {
|
|||||||
<div role="heading" aria-level="1" className="h2">{title}</div>
|
<div role="heading" aria-level="1" className="h2">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
|
|
||||||
<div className="row course-outline-tab">
|
<div className="row course-outline-tab">
|
||||||
<AccountActivationAlert />
|
<AccountActivationAlert />
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
@@ -146,21 +139,17 @@ function OutlineTab({ intl }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col col-12 col-md-8">
|
<div className="col col-12 col-md-8">
|
||||||
{ /** [MM-P2P] Experiment (the conditional) */ }
|
<AlertList
|
||||||
{ !MMP2P.state.isEnabled
|
topic="outline-course-alerts"
|
||||||
&& (
|
className="mb-3"
|
||||||
<AlertList
|
customAlerts={{
|
||||||
topic="outline-course-alerts"
|
...certificateAvailableAlert,
|
||||||
className="mb-3"
|
...courseEndAlert,
|
||||||
customAlerts={{
|
...courseStartAlert,
|
||||||
...certificateAvailableAlert,
|
...scheduledContentAlert,
|
||||||
...courseEndAlert,
|
}}
|
||||||
...courseStartAlert,
|
/>
|
||||||
...scheduledContentAlert,
|
{isSelfPaced && hasDeadlines && (
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isSelfPaced && hasDeadlines && !MMP2P.state.isEnabled && (
|
|
||||||
<>
|
<>
|
||||||
<ShiftDatesAlert model="outline" fetch={fetchOutlineTab} />
|
<ShiftDatesAlert model="outline" fetch={fetchOutlineTab} />
|
||||||
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
|
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
|
||||||
@@ -203,35 +192,27 @@ function OutlineTab({ intl }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CourseTools />
|
<CourseTools />
|
||||||
{ /** [MM-P2P] Experiment (conditional) */ }
|
<UpgradeNotification
|
||||||
{ MMP2P.state.isEnabled
|
offer={offer}
|
||||||
? <MMP2PFlyover isStatic options={MMP2P} />
|
verifiedMode={verifiedMode}
|
||||||
: (
|
accessExpiration={accessExpiration}
|
||||||
<UpgradeNotification
|
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||||
offer={offer}
|
marketingUrl={marketingUrl}
|
||||||
verifiedMode={verifiedMode}
|
upsellPageName="course_home"
|
||||||
accessExpiration={accessExpiration}
|
userTimezone={userTimezone}
|
||||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
shouldDisplayBorder
|
||||||
marketingUrl={marketingUrl}
|
timeOffsetMillis={timeOffsetMillis}
|
||||||
upsellPageName="course_home"
|
courseId={courseId}
|
||||||
userTimezone={userTimezone}
|
org={org}
|
||||||
shouldDisplayBorder
|
|
||||||
timeOffsetMillis={timeOffsetMillis}
|
|
||||||
courseId={courseId}
|
|
||||||
org={org}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CourseDates
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p={MMP2P}
|
|
||||||
/>
|
/>
|
||||||
|
<CourseDates />
|
||||||
<CourseHandouts />
|
<CourseHandouts />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
OutlineTab.propTypes = {
|
OutlineTab.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -119,11 +119,11 @@ describe('Outline Tab', () => {
|
|||||||
|
|
||||||
// Click to expand section
|
// Click to expand section
|
||||||
userEvent.click(expandButton);
|
userEvent.click(expandButton);
|
||||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true');
|
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
||||||
|
|
||||||
// Click to collapse section
|
// Click to collapse section
|
||||||
userEvent.click(expandButton);
|
userEvent.click(expandButton);
|
||||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays correct icon for complete assignment', async () => {
|
it('displays correct icon for complete assignment', async () => {
|
||||||
@@ -355,13 +355,13 @@ describe('Outline Tab', () => {
|
|||||||
|
|
||||||
await fetchAndRender('http://localhost/?weekly_goal=3');
|
await fetchAndRender('http://localhost/?weekly_goal=3');
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('welcome.email.clicked.setgoal', {});
|
expect(sendTrackEvent).toHaveBeenCalledWith('enrollment.email.clicked.setgoal', {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emit start course event via query param', async () => {
|
it('emit start course event via query param', async () => {
|
||||||
sendTrackEvent.mockClear();
|
sendTrackEvent.mockClear();
|
||||||
await fetchAndRender('http://localhost/?start_course=1');
|
await fetchAndRender('http://localhost/?start_course=1');
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('welcome.email.clicked.startcourse', {});
|
expect(sendTrackEvent).toHaveBeenCalledWith('enrollment.email.clicked.startcourse', {});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('weekly learning goal is not set', () => {
|
describe('weekly learning goal is not set', () => {
|
||||||
@@ -383,25 +383,25 @@ describe('Outline Tab', () => {
|
|||||||
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeDisabled();
|
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each([
|
||||||
level | days
|
{ level: 'Casual', days: 1 },
|
||||||
${'Casual'} | ${1}
|
{ level: 'Regular', days: 3 },
|
||||||
${'Regular'} | ${3}
|
{ level: 'Intense', days: 5 },
|
||||||
${'Intense'} | ${5}
|
])('calls the API with a goal of $days when $level goal is clicked', async ({ level, days }) => {
|
||||||
`('calls the API with a goal of $days when $level goal is clicked', async ({ level, days }) => {
|
// click on Casual goal
|
||||||
// click on Casual goal
|
const button = await screen.queryByTestId(`weekly-learning-goal-input-${level}`);
|
||||||
const button = await screen.queryByTestId(`weekly-learning-goal-input-${level}`);
|
fireEvent.click(button);
|
||||||
fireEvent.click(button);
|
// Verify the request was made
|
||||||
// Verify the request was made
|
await waitFor(() => {
|
||||||
await waitFor(() => {
|
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
||||||
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
// subscribe is turned on automatically
|
||||||
// subscribe is turned on automatically
|
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","days_per_week":${days},"subscribed_to_reminders":true}`);
|
||||||
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","days_per_week":${days},"subscribed_to_reminders":true}`);
|
// verify that the additional info about subscriptions shows up
|
||||||
// verify that the additional info about subscriptions shows up
|
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument();
|
||||||
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument();
|
});
|
||||||
});
|
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled();
|
||||||
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled();
|
});
|
||||||
});
|
|
||||||
it('shows and hides subscribe to reminders additional text', async () => {
|
it('shows and hides subscribe to reminders additional text', async () => {
|
||||||
const button = await screen.getByTestId('weekly-learning-goal-input-Regular');
|
const button = await screen.getByTestId('weekly-learning-goal-input-Regular');
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
@@ -789,12 +789,14 @@ describe('Outline Tab', () => {
|
|||||||
const requestingButton = screen.getByRole('button', { name: 'Request certificate' });
|
const requestingButton = screen.getByRole('button', { name: 'Request certificate' });
|
||||||
fireEvent.click(requestingButton);
|
fireEvent.click(requestingButton);
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked',
|
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||||
|
'edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked',
|
||||||
{
|
{
|
||||||
courserun_key: courseId,
|
courserun_key: courseId,
|
||||||
is_staff: false,
|
is_staff: false,
|
||||||
org_key: 'edX',
|
org_key: 'edX',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tracks unverified cert button', async () => {
|
it('tracks unverified cert button', async () => {
|
||||||
@@ -833,12 +835,14 @@ describe('Outline Tab', () => {
|
|||||||
const requestingButton = screen.getByRole('link', { name: 'Verify my ID' });
|
const requestingButton = screen.getByRole('link', { name: 'Verify my ID' });
|
||||||
fireEvent.click(requestingButton);
|
fireEvent.click(requestingButton);
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_unverified_button.clicked',
|
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||||
|
'edx.ui.lms.course_outline.certificate_alert_unverified_button.clicked',
|
||||||
{
|
{
|
||||||
courserun_key: courseId,
|
courserun_key: courseId,
|
||||||
is_staff: false,
|
is_staff: false,
|
||||||
org_key: 'edX',
|
org_key: 'edX',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import genericMessages from '../../generic/messages';
|
import genericMessages from '../../generic/messages';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function Section({
|
const Section = ({
|
||||||
courseId,
|
courseId,
|
||||||
defaultOpen,
|
defaultOpen,
|
||||||
expand,
|
expand,
|
||||||
intl,
|
intl,
|
||||||
section,
|
section,
|
||||||
}) {
|
}) => {
|
||||||
const {
|
const {
|
||||||
complete,
|
complete,
|
||||||
sequenceIds,
|
sequenceIds,
|
||||||
@@ -38,6 +38,7 @@ function Section({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(defaultOpen);
|
setOpen(defaultOpen);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sectionTitle = (
|
const sectionTitle = (
|
||||||
@@ -109,7 +110,7 @@ function Section({
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Section.propTypes = {
|
Section.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import EffortEstimate from '../../shared/effort-estimate';
|
|||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function SequenceLink({
|
const SequenceLink = ({
|
||||||
id,
|
id,
|
||||||
intl,
|
intl,
|
||||||
courseId,
|
courseId,
|
||||||
first,
|
first,
|
||||||
sequence,
|
sequence,
|
||||||
}) {
|
}) => {
|
||||||
const {
|
const {
|
||||||
complete,
|
complete,
|
||||||
description,
|
description,
|
||||||
@@ -39,6 +39,50 @@ function SequenceLink({
|
|||||||
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
||||||
const displayTitle = showLink ? coursewareUrl : title;
|
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 (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||||
@@ -70,35 +114,15 @@ function SequenceLink({
|
|||||||
<EffortEstimate className="ml-3 align-middle" block={sequence} />
|
<EffortEstimate className="ml-3 align-middle" block={sequence} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{due && (
|
<div className="row w-100 m-0 ml-3 pl-3">
|
||||||
<div className="row w-100 m-0 ml-3 pl-3">
|
<small className="text-body pl-2">
|
||||||
<small className="text-body pl-2">
|
{due ? dueDateMessage : noDueDateMessage}
|
||||||
<FormattedMessage
|
</small>
|
||||||
id="learning.outline.sequence-due"
|
</div>
|
||||||
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 || '',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
SequenceLink.propTypes = {
|
SequenceLink.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const CERT_STATUS_TYPE = {
|
|||||||
UNVERIFIED: 'unverified',
|
UNVERIFIED: 'unverified',
|
||||||
};
|
};
|
||||||
|
|
||||||
function CertificateStatusAlert({ intl, payload }) {
|
const CertificateStatusAlert = ({ intl, payload }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
certificateAvailableDate,
|
certificateAvailableDate,
|
||||||
@@ -189,7 +189,7 @@ function CertificateStatusAlert({ intl, payload }) {
|
|||||||
)}
|
)}
|
||||||
</AlertWrapper>
|
</AlertWrapper>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CertificateStatusAlert.propTypes = {
|
CertificateStatusAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function useCertificateStatusAlert(courseId) {
|
|||||||
&& hasEnded
|
&& hasEnded
|
||||||
&& !userHasPassingGrade
|
&& !userHasPassingGrade
|
||||||
);
|
);
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
certificateAvailableDate,
|
certificateAvailableDate,
|
||||||
certURL,
|
certURL,
|
||||||
certStatus,
|
certStatus,
|
||||||
@@ -85,11 +85,12 @@ function useCertificateStatusAlert(courseId) {
|
|||||||
org,
|
org,
|
||||||
notPassingCourseEnded,
|
notPassingCourseEnded,
|
||||||
tabs,
|
tabs,
|
||||||
};
|
}), [certStatus, certURL, certificateAvailableDate, courseId,
|
||||||
|
endBlock, notPassingCourseEnded, org, tabs, userTimezone]);
|
||||||
|
|
||||||
useAlert(isVisible || notPassingCourseEnded, {
|
useAlert(isVisible || notPassingCourseEnded, {
|
||||||
code: 'clientCertificateStatusAlert',
|
code: 'clientCertificateStatusAlert',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'outline-course-alerts',
|
topic: 'outline-course-alerts',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import {
|
|||||||
import { Alert } from '@edx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
import { Info } from '@edx/paragon/icons';
|
import { Info } from '@edx/paragon/icons';
|
||||||
|
|
||||||
const DAY_MS = 24 * 60 * 60 * 1000; // in ms
|
const DAY_SEC = 24 * 60 * 60; // in seconds
|
||||||
|
const DAY_MS = DAY_SEC * 1000; // in ms
|
||||||
|
const YEAR_SEC = 365 * DAY_SEC; // in seconds
|
||||||
|
|
||||||
function CourseEndAlert({ payload }) {
|
const CourseEndAlert = ({ payload }) => {
|
||||||
const {
|
const {
|
||||||
description,
|
description,
|
||||||
endDate,
|
endDate,
|
||||||
@@ -20,16 +22,19 @@ function CourseEndAlert({ payload }) {
|
|||||||
|
|
||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
|
let msg;
|
||||||
|
const delta = new Date(endDate) - new Date();
|
||||||
const timeRemaining = (
|
const timeRemaining = (
|
||||||
<FormattedRelativeTime
|
<FormattedRelativeTime
|
||||||
key="timeRemaining"
|
key="timeRemaining"
|
||||||
value={endDate}
|
value={delta / 1000}
|
||||||
|
numeric="auto"
|
||||||
|
// 1 year interval to help auto format. It won't format without updateIntervalInSeconds.
|
||||||
|
updateIntervalInSeconds={YEAR_SEC}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
let msg;
|
|
||||||
const delta = new Date(endDate) - new Date();
|
|
||||||
if (delta < DAY_MS) {
|
if (delta < DAY_MS) {
|
||||||
const courseEndTime = (
|
const courseEndTime = (
|
||||||
<FormattedTime
|
<FormattedTime
|
||||||
@@ -83,7 +88,7 @@ function CourseEndAlert({ payload }) {
|
|||||||
{description}
|
{description}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseEndAlert.propTypes = {
|
CourseEndAlert.propTypes = {
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ export function useCourseEndAlert(courseId) {
|
|||||||
const endDate = endBlock ? new Date(endBlock.date) : null;
|
const endDate = endBlock ? new Date(endBlock.date) : null;
|
||||||
const delta = endBlock ? endDate - new Date() : 0;
|
const delta = endBlock ? endDate - new Date() : 0;
|
||||||
const isVisible = isEnrolled && endBlock && delta > 0 && delta < WARNING_PERIOD_MS;
|
const isVisible = isEnrolled && endBlock && delta > 0 && delta < WARNING_PERIOD_MS;
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
description: endBlock && endBlock.description,
|
description: endBlock && endBlock.description,
|
||||||
endDate: endBlock && endBlock.date,
|
endDate: endBlock && endBlock.date,
|
||||||
userTimezone,
|
userTimezone,
|
||||||
};
|
}), [endBlock, userTimezone]);
|
||||||
|
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientCourseEndAlert',
|
code: 'clientCourseEndAlert',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'outline-course-alerts',
|
topic: 'outline-course-alerts',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +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';
|
||||||
|
|
||||||
function PrivateCourseAlert({ intl, payload }) {
|
const PrivateCourseAlert = ({ intl, payload }) => {
|
||||||
const {
|
const {
|
||||||
anonymousUser,
|
anonymousUser,
|
||||||
canEnroll,
|
canEnroll,
|
||||||
@@ -100,7 +100,7 @@ function PrivateCourseAlert({ intl, payload }) {
|
|||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
PrivateCourseAlert.propTypes = {
|
PrivateCourseAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ export function usePrivateCourseAlert(courseId) {
|
|||||||
* 2. the user is authenticated.
|
* 2. the user is authenticated.
|
||||||
* */
|
* */
|
||||||
const isVisible = !enrolledUser && (privateOutline || authenticatedUser !== null);
|
const isVisible = !enrolledUser && (privateOutline || authenticatedUser !== null);
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
anonymousUser: authenticatedUser === null,
|
anonymousUser: authenticatedUser === null,
|
||||||
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
||||||
courseId,
|
courseId,
|
||||||
};
|
}), [authenticatedUser, courseId, outline]);
|
||||||
|
|
||||||
useAlert(isVisible, {
|
useAlert(isVisible, {
|
||||||
code: 'clientPrivateCourseAlert',
|
code: 'clientPrivateCourseAlert',
|
||||||
dismissible: false,
|
dismissible: false,
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'outline-private-alerts',
|
topic: 'outline-private-alerts',
|
||||||
type: ALERT_TYPES.WELCOME,
|
type: ALERT_TYPES.WELCOME,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Alert, Button } from '@edx/paragon';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
function ScheduledContentAlert({ payload }) {
|
const ScheduledContentAlert = ({ payload }) => {
|
||||||
const {
|
const {
|
||||||
datesTabLink,
|
datesTabLink,
|
||||||
} = payload;
|
} = payload;
|
||||||
@@ -38,7 +38,7 @@ function ScheduledContentAlert({ payload }) {
|
|||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ScheduledContentAlert.propTypes = {
|
ScheduledContentAlert.propTypes = {
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ const useScheduledContentAlert = (courseId) => {
|
|||||||
&& !!Object.values(courses).find(course => course.hasScheduledContent === true)
|
&& !!Object.values(courses).find(course => course.hasScheduledContent === true)
|
||||||
);
|
);
|
||||||
const { isEnrolled } = useModel('courseHomeMeta', courseId);
|
const { isEnrolled } = useModel('courseHomeMeta', courseId);
|
||||||
const payload = {
|
const payload = useMemo(() => ({
|
||||||
datesTabLink,
|
datesTabLink,
|
||||||
};
|
}), [datesTabLink]);
|
||||||
useAlert(hasScheduledContent && isEnrolled, {
|
useAlert(hasScheduledContent && isEnrolled, {
|
||||||
code: 'ScheduledContentAlert',
|
code: 'ScheduledContentAlert',
|
||||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
payload,
|
||||||
topic: 'outline-course-alerts',
|
topic: 'outline-course-alerts',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
@@ -8,11 +7,9 @@ import DateSummary from '../DateSummary';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
function CourseDates({
|
const CourseDates = ({
|
||||||
intl,
|
intl,
|
||||||
/** [MM-P2P] Experiment */
|
}) => {
|
||||||
mmp2p,
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -40,8 +37,6 @@ function CourseDates({
|
|||||||
key={courseDateBlock.title + courseDateBlock.date}
|
key={courseDateBlock.title + courseDateBlock.date}
|
||||||
dateBlock={courseDateBlock}
|
dateBlock={courseDateBlock}
|
||||||
userTimezone={userTimezone}
|
userTimezone={userTimezone}
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p={mmp2p}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -51,17 +46,10 @@ function CourseDates({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseDates.propTypes = {
|
CourseDates.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: PropTypes.shape({}),
|
|
||||||
};
|
|
||||||
|
|
||||||
CourseDates.defaultProps = {
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CourseDates);
|
export default injectIntl(CourseDates);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import LmsHtmlFragment from '../LmsHtmlFragment';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
function CourseHandouts({ intl }) {
|
const CourseHandouts = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -29,7 +29,7 @@ function CourseHandouts({ intl }) {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseHandouts.propTypes = {
|
CourseHandouts.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -14,7 +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';
|
||||||
|
|
||||||
function CourseTools({ intl }) {
|
const CourseTools = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -79,7 +79,7 @@ function CourseTools({ intl }) {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseTools.propTypes = {
|
CourseTools.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -2,35 +2,35 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
function FlagButton({
|
const FlagButton = ({
|
||||||
buttonIcon,
|
buttonIcon,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
isSelected,
|
isSelected,
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={classnames(
|
||||||
className={classnames('flag-button row w-100 align-content-between m-1.5 py-3.5',
|
'flag-button row w-100 align-content-between m-1.5 py-3.5',
|
||||||
isSelected ? 'flag-button-selected' : '')}
|
isSelected ? 'flag-button-selected' : '',
|
||||||
aria-checked={isSelected}
|
)}
|
||||||
role="radio"
|
aria-checked={isSelected}
|
||||||
onClick={() => handleSelect()}
|
role="radio"
|
||||||
data-testid={`weekly-learning-goal-input-${title}`}
|
onClick={() => handleSelect()}
|
||||||
>
|
data-testid={`weekly-learning-goal-input-${title}`}
|
||||||
<div className="row w-100 m-0 justify-content-center pb-1">
|
>
|
||||||
{buttonIcon}
|
<div className="row w-100 m-0 justify-content-center pb-1">
|
||||||
</div>
|
{buttonIcon}
|
||||||
<div className={classnames('row w-100 m-0 justify-content-center small text-gray-700 pb-1', isSelected ? 'font-weight-bold' : '')}>
|
</div>
|
||||||
{title}
|
<div className={classnames('row w-100 m-0 justify-content-center small text-gray-700 pb-1', isSelected ? 'font-weight-bold' : '')}>
|
||||||
</div>
|
{title}
|
||||||
<div className={classnames('row w-100 m-0 justify-content-center micro text-gray-500', isSelected ? 'font-weight-bold' : '')}>
|
</div>
|
||||||
{text}
|
<div className={classnames('row w-100 m-0 justify-content-center micro text-gray-500', isSelected ? 'font-weight-bold' : '')}>
|
||||||
</div>
|
{text}
|
||||||
</button>
|
</div>
|
||||||
);
|
</button>
|
||||||
}
|
);
|
||||||
|
|
||||||
FlagButton.propTypes = {
|
FlagButton.propTypes = {
|
||||||
buttonIcon: PropTypes.element.isRequired,
|
buttonIcon: PropTypes.element.isRequired,
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import { ReactComponent as FlagRegularIcon } from './flag_gray.svg';
|
|||||||
import FlagButton from './FlagButton';
|
import FlagButton from './FlagButton';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function LearningGoalButton({
|
const LearningGoalButton = ({
|
||||||
level,
|
level,
|
||||||
isSelected,
|
isSelected,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
intl,
|
intl,
|
||||||
}) {
|
}) => {
|
||||||
const buttonDetails = {
|
const buttonDetails = {
|
||||||
casual: {
|
casual: {
|
||||||
daysPerWeek: 1,
|
daysPerWeek: 1,
|
||||||
@@ -47,7 +47,7 @@ function LearningGoalButton({
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
LearningGoalButton.propTypes = {
|
LearningGoalButton.propTypes = {
|
||||||
level: PropTypes.string.isRequired,
|
level: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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';
|
||||||
|
|
||||||
function ProctoringInfoPanel({ intl }) {
|
const ProctoringInfoPanel = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -128,6 +128,7 @@ function ProctoringInfoPanel({ intl }) {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
dispatch(fetchProctoringInfoResolved());
|
dispatch(fetchProctoringInfoResolved());
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let onboardingExamButton = null;
|
let onboardingExamButton = null;
|
||||||
@@ -170,6 +171,7 @@ function ProctoringInfoPanel({ intl }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
<>
|
<>
|
||||||
{ showInfoPanel && (
|
{ showInfoPanel && (
|
||||||
<section className={`mb-4 p-3 outline-sidebar-proctoring-panel ${getBorderClass()}`}>
|
<section className={`mb-4 p-3 outline-sidebar-proctoring-panel ${getBorderClass()}`}>
|
||||||
@@ -212,7 +214,7 @@ function ProctoringInfoPanel({ intl }) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ProctoringInfoPanel.propTypes = {
|
ProctoringInfoPanel.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
function StartOrResumeCourseCard({ intl }) {
|
const StartOrResumeCourseCard = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -56,10 +56,11 @@ function StartOrResumeCourseCard({ intl }) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/* Footer is needed for internal vertical spacing to work out. If you can remove, be my guest */}
|
{/* Footer is needed for internal vertical spacing to work out. If you can remove, be my guest */}
|
||||||
|
{/* eslint-disable-next-line react/jsx-no-useless-fragment */}
|
||||||
<Card.Footer><></></Card.Footer>
|
<Card.Footer><></></Card.Footer>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
StartOrResumeCourseCard.propTypes = {
|
StartOrResumeCourseCard.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import { saveWeeklyLearningGoal } from '../../data';
|
|||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import './FlagButton.scss';
|
import './FlagButton.scss';
|
||||||
|
|
||||||
function WeeklyLearningGoalCard({
|
const WeeklyLearningGoalCard = ({
|
||||||
daysPerWeek,
|
daysPerWeek,
|
||||||
subscribedToReminders,
|
subscribedToReminders,
|
||||||
intl,
|
intl,
|
||||||
}) {
|
}) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -36,7 +36,7 @@ function WeeklyLearningGoalCard({
|
|||||||
const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders);
|
const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
function handleSelect(days, triggeredFromEmail = false) {
|
const handleSelect = (days, triggeredFromEmail = false) => {
|
||||||
// Set the subscription button if this is the first time selecting a goal
|
// Set the subscription button if this is the first time selecting a goal
|
||||||
const selectReminders = daysPerWeekGoal === null ? true : isGetReminderSelected;
|
const selectReminders = daysPerWeekGoal === null ? true : isGetReminderSelected;
|
||||||
setGetReminderSelected(selectReminders);
|
setGetReminderSelected(selectReminders);
|
||||||
@@ -51,10 +51,10 @@ function WeeklyLearningGoalCard({
|
|||||||
reminder_selected: selectReminders,
|
reminder_selected: selectReminders,
|
||||||
});
|
});
|
||||||
if (triggeredFromEmail) {
|
if (triggeredFromEmail) {
|
||||||
sendTrackEvent('welcome.email.clicked.setgoal', {});
|
sendTrackEvent('enrollment.email.clicked.setgoal', {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function handleSubscribeToReminders(event) {
|
function handleSubscribeToReminders(event) {
|
||||||
const isGetReminderChecked = event.target.checked;
|
const isGetReminderChecked = event.target.checked;
|
||||||
@@ -84,6 +84,7 @@ function WeeklyLearningGoalCard({
|
|||||||
search: currentParams.toString(),
|
search: currentParams.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [location.search]);
|
}, [location.search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -146,7 +147,7 @@ function WeeklyLearningGoalCard({
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
WeeklyLearningGoalCard.propTypes = {
|
WeeklyLearningGoalCard.propTypes = {
|
||||||
daysPerWeek: PropTypes.number,
|
daysPerWeek: PropTypes.number,
|
||||||
|
|||||||
@@ -11,21 +11,22 @@ import messages from '../messages';
|
|||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import { dismissWelcomeMessage } from '../../data/thunks';
|
import { dismissWelcomeMessage } from '../../data/thunks';
|
||||||
|
|
||||||
function WelcomeMessage({ courseId, intl }) {
|
const WelcomeMessage = ({ courseId, intl }) => {
|
||||||
const {
|
const {
|
||||||
welcomeMessageHtml,
|
welcomeMessageHtml,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
if (!welcomeMessageHtml) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [display, setDisplay] = useState(true);
|
const [display, setDisplay] = useState(true);
|
||||||
|
|
||||||
const shortWelcomeMessageHtml = truncate(welcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true });
|
const shortWelcomeMessageHtml = truncate(welcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true });
|
||||||
const messageCanBeShortened = shortWelcomeMessageHtml.length < welcomeMessageHtml.length;
|
const messageCanBeShortened = shortWelcomeMessageHtml.length < welcomeMessageHtml.length;
|
||||||
const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened);
|
const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
if (!welcomeMessageHtml) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
data-testid="alert-container-welcome"
|
data-testid="alert-container-welcome"
|
||||||
@@ -69,7 +70,7 @@ function WelcomeMessage({ courseId, intl }) {
|
|||||||
</TransitionReplace>
|
</TransitionReplace>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
WelcomeMessage.propTypes = {
|
WelcomeMessage.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useModel } from '../../generic/model-store';
|
|||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function ProgressHeader({ intl }) {
|
const ProgressHeader = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
targetUserId,
|
targetUserId,
|
||||||
@@ -26,18 +26,16 @@ function ProgressHeader({ intl }) {
|
|||||||
: intl.formatMessage(messages.progressHeader);
|
: intl.formatMessage(messages.progressHeader);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="row w-100 m-0 mt-3 mb-4 justify-content-between">
|
||||||
<div className="row w-100 m-0 mt-3 mb-4 justify-content-between">
|
<h1>{pageTitle}</h1>
|
||||||
<h1>{pageTitle}</h1>
|
{administrator && studioUrl && (
|
||||||
{administrator && studioUrl && (
|
<Button variant="outline-primary" size="sm" className="align-self-center" href={studioUrl}>
|
||||||
<Button variant="outline-primary" size="sm" className="align-self-center" href={studioUrl}>
|
{intl.formatMessage(messages.studioLink)}
|
||||||
{intl.formatMessage(messages.studioLink)}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ProgressHeader.propTypes = {
|
ProgressHeader.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import RelatedLinks from './related-links/RelatedLinks';
|
|||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
function ProgressTab() {
|
const ProgressTab = () => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -55,6 +55,6 @@ function ProgressTab() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProgressTab;
|
export default ProgressTab;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../
|
|||||||
import { requestCert } from '../../data/thunks';
|
import { requestCert } from '../../data/thunks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function CertificateStatus({ intl }) {
|
const CertificateStatus = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -206,6 +206,7 @@ function CertificateStatus({ intl }) {
|
|||||||
grade_variant: gradeEventName,
|
grade_variant: gradeEventName,
|
||||||
certificate_status_variant: certEventName,
|
certificate_status_variant: certEventName,
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!certCase) {
|
if (!certCase) {
|
||||||
@@ -257,7 +258,7 @@ function CertificateStatus({ intl }) {
|
|||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CertificateStatus.propTypes = {
|
CertificateStatus.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
|
|||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
|
const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) => {
|
||||||
|
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
||||||
|
|
||||||
if (!completePercentage) {
|
if (!completePercentage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
|
||||||
|
|
||||||
const completeSegmentOffset = (3.6 * completePercentage) / 8;
|
const completeSegmentOffset = (3.6 * completePercentage) / 8;
|
||||||
let completeTooltipDegree = completePercentage < 100 ? -completeSegmentOffset : 0;
|
let completeTooltipDegree = completePercentage < 100 ? -completeSegmentOffset : 0;
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
|
|||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CompleteDonutSegment.propTypes = {
|
CompleteDonutSegment.propTypes = {
|
||||||
completePercentage: PropTypes.number.isRequired,
|
completePercentage: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import IncompleteDonutSegment from './IncompleteDonutSegment';
|
|||||||
import LockedDonutSegment from './LockedDonutSegment';
|
import LockedDonutSegment from './LockedDonutSegment';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function CompletionDonutChart({ intl }) {
|
const CompletionDonutChart = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -60,7 +60,7 @@ function CompletionDonutChart({ intl }) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CompletionDonutChart.propTypes = {
|
CompletionDonutChart.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -4,23 +4,21 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|||||||
import CompletionDonutChart from './CompletionDonutChart';
|
import CompletionDonutChart from './CompletionDonutChart';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function CourseCompletion({ intl }) {
|
const CourseCompletion = ({ intl }) => (
|
||||||
return (
|
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
||||||
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
<div className="row w-100 m-0">
|
||||||
<div className="row w-100 m-0">
|
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||||
<div className="col-12 col-sm-6 col-md-7 p-0">
|
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||||
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
<p className="small">
|
||||||
<p className="small">
|
{intl.formatMessage(messages.completionBody)}
|
||||||
{intl.formatMessage(messages.completionBody)}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
|
||||||
<CompletionDonutChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
||||||
);
|
<CompletionDonutChart />
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
CourseCompletion.propTypes = {
|
CourseCompletion.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
|
|||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function IncompleteDonutSegment({ incompletePercentage, intl }) {
|
const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
||||||
|
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
||||||
|
|
||||||
if (!incompletePercentage) {
|
if (!incompletePercentage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
|
||||||
|
|
||||||
const incompleteSegmentOffset = (3.6 * incompletePercentage) / 16;
|
const incompleteSegmentOffset = (3.6 * incompletePercentage) / 16;
|
||||||
const incompleteTooltipDegree = incompletePercentage < 100 ? incompleteSegmentOffset : 0;
|
const incompleteTooltipDegree = incompletePercentage < 100 ? incompleteSegmentOffset : 0;
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ function IncompleteDonutSegment({ incompletePercentage, intl }) {
|
|||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
IncompleteDonutSegment.propTypes = {
|
IncompleteDonutSegment.propTypes = {
|
||||||
incompletePercentage: PropTypes.number.isRequired,
|
incompletePercentage: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function LockedDonutSegment({ intl, lockedPercentage }) {
|
const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
||||||
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
||||||
|
|
||||||
if (!lockedPercentage) {
|
if (!lockedPercentage) {
|
||||||
@@ -62,7 +62,7 @@ function LockedDonutSegment({ intl, lockedPercentage }) {
|
|||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
LockedDonutSegment.propTypes = {
|
LockedDonutSegment.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { DashboardLink } from '../../../shared/links';
|
|||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function CreditInformation({ intl }) {
|
const CreditInformation = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -106,7 +106,7 @@ function CreditInformation({ intl }) {
|
|||||||
{requirements}
|
{requirements}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CreditInformation.propTypes = {
|
CreditInformation.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import CreditInformation from '../../credit-information/CreditInformation';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function CourseGrade({ intl }) {
|
const CourseGrade = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -52,7 +52,7 @@ function CourseGrade({ intl }) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseGrade.propTypes = {
|
CourseGrade.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function CourseGradeFooter({ intl, passingGrade }) {
|
const CourseGradeFooter = ({ intl, passingGrade }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -83,7 +83,7 @@ function CourseGradeFooter({ intl, passingGrade }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseGradeFooter.propTypes = {
|
CourseGradeFooter.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Button, Icon } from '@edx/paragon';
|
|||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function CourseGradeHeader({ intl }) {
|
const CourseGradeHeader = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -81,7 +81,7 @@ function CourseGradeHeader({ intl }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseGradeHeader.propTypes = {
|
CourseGradeHeader.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
const CurrentGradeTooltip = ({ intl, tooltipClassName }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -62,7 +62,7 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
|||||||
</text>
|
</text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CurrentGradeTooltip.defaultProps = {
|
CurrentGradeTooltip.defaultProps = {
|
||||||
tooltipClassName: '',
|
tooltipClassName: '',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import PassingGradeTooltip from './PassingGradeTooltip';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function GradeBar({ intl, passingGrade }) {
|
const GradeBar = ({ intl, passingGrade }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -49,7 +49,7 @@ function GradeBar({ intl, passingGrade }) {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
GradeBar.propTypes = {
|
GradeBar.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
|
const GradeRangeTooltip = ({ intl, iconButtonClassName, passingGrade }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -72,7 +72,7 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
|
|||||||
/>
|
/>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
GradeRangeTooltip.defaultProps = {
|
GradeRangeTooltip.defaultProps = {
|
||||||
iconButtonClassName: '',
|
iconButtonClassName: '',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
const PassingGradeTooltip = ({ intl, passingGrade, tooltipClassName }) => {
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
||||||
@@ -47,7 +47,7 @@ function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
|||||||
</text>
|
</text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
PassingGradeTooltip.defaultProps = {
|
PassingGradeTooltip.defaultProps = {
|
||||||
tooltipClassName: '',
|
tooltipClassName: '',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import DetailedGradesTable from './DetailedGradesTable';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function DetailedGrades({ intl }) {
|
const DetailedGrades = ({ intl }) => {
|
||||||
const { administrator } = getAuthenticatedUser();
|
const { administrator } = getAuthenticatedUser();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
@@ -79,7 +79,7 @@ function DetailedGrades({ intl }) {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
DetailedGrades.propTypes = {
|
DetailedGrades.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import SubsectionTitleCell from './SubsectionTitleCell';
|
import SubsectionTitleCell from './SubsectionTitleCell';
|
||||||
|
|
||||||
function DetailedGradesTable({ intl }) {
|
const DetailedGradesTable = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -64,7 +64,7 @@ function DetailedGradesTable({ intl }) {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
DetailedGradesTable.propTypes = {
|
DetailedGradesTable.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function ProblemScoreDrawer({ intl, problemScores, subsection }) {
|
const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
return (
|
return (
|
||||||
<span className="row w-100 m-0 x-small ml-4 pt-2 pl-1 text-gray-700 flex-nowrap">
|
<span className="row w-100 m-0 x-small ml-4 pt-2 pl-1 text-gray-700 flex-nowrap">
|
||||||
@@ -22,7 +22,7 @@ function ProblemScoreDrawer({ intl, problemScores, subsection }) {
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ProblemScoreDrawer.propTypes = {
|
ProblemScoreDrawer.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import messages from '../messages';
|
|||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import ProblemScoreDrawer from './ProblemScoreDrawer';
|
import ProblemScoreDrawer from './ProblemScoreDrawer';
|
||||||
|
|
||||||
function SubsectionTitleCell({ intl, subsection }) {
|
const SubsectionTitleCell = ({ intl, subsection }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -99,7 +99,7 @@ function SubsectionTitleCell({ intl, subsection }) {
|
|||||||
</Collapsible.Body>
|
</Collapsible.Body>
|
||||||
</Collapsible.Advanced>
|
</Collapsible.Advanced>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
SubsectionTitleCell.propTypes = {
|
SubsectionTitleCell.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { Icon } from '@edx/paragon';
|
|||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function AssignmentTypeCell({
|
const AssignmentTypeCell = ({
|
||||||
intl, assignmentType, footnoteMarker, footnoteId, locked,
|
intl, assignmentType, footnoteMarker, footnoteId, locked,
|
||||||
}) {
|
}) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -42,7 +42,7 @@ function AssignmentTypeCell({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
AssignmentTypeCell.propTypes = {
|
AssignmentTypeCell.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
function DroppableAssignmentFootnote({ footnotes, intl }) {
|
const DroppableAssignmentFootnote = ({ footnotes, intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -37,7 +37,7 @@ function DroppableAssignmentFootnote({ footnotes, intl }) {
|
|||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
DroppableAssignmentFootnote.propTypes = {
|
DroppableAssignmentFootnote.propTypes = {
|
||||||
footnotes: PropTypes.arrayOf(PropTypes.shape({
|
footnotes: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
import GradeSummaryHeader from './GradeSummaryHeader';
|
import GradeSummaryHeader from './GradeSummaryHeader';
|
||||||
import GradeSummaryTable from './GradeSummaryTable';
|
import GradeSummaryTable from './GradeSummaryTable';
|
||||||
|
|
||||||
function GradeSummary() {
|
const GradeSummary = () => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -28,6 +28,6 @@ function GradeSummary() {
|
|||||||
<GradeSummaryTable setAllOfSomeAssignmentTypeIsLocked={setAllOfSomeAssignmentTypeIsLocked} />
|
<GradeSummaryTable setAllOfSomeAssignmentTypeIsLocked={setAllOfSomeAssignmentTypeIsLocked} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default GradeSummary;
|
export default GradeSummary;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Blocked, InfoOutline } from '@edx/paragon/icons';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
function GradeSummaryHeader({ intl, allOfSomeAssignmentTypeIsLocked }) {
|
const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -54,7 +54,7 @@ function GradeSummaryHeader({ intl, allOfSomeAssignmentTypeIsLocked }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
GradeSummaryHeader.propTypes = {
|
GradeSummaryHeader.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import GradeSummaryTableFooter from './GradeSummaryTableFooter';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -79,6 +79,16 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
|||||||
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const getAssignmentTypeCell = (value) => (
|
||||||
|
<AssignmentTypeCell
|
||||||
|
assignmentType={value.type} // eslint-disable-line react/prop-types
|
||||||
|
footnoteId={value.footnoteId} // eslint-disable-line react/prop-types
|
||||||
|
footnoteMarker={value.footnoteMarker} // eslint-disable-line react/prop-types
|
||||||
|
locked={value.locked} // eslint-disable-line react/prop-types
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCell = (locked, value) => <span className={locked ? 'greyed-out' : ''}>{value}</span>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -89,45 +99,28 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
|||||||
{
|
{
|
||||||
Header: `${intl.formatMessage(messages.assignmentType)}`,
|
Header: `${intl.formatMessage(messages.assignmentType)}`,
|
||||||
accessor: 'type',
|
accessor: 'type',
|
||||||
// eslint-disable-next-line react/prop-types
|
Cell: ({ value }) => getAssignmentTypeCell(value),
|
||||||
Cell: ({ value }) => (
|
|
||||||
<AssignmentTypeCell
|
|
||||||
assignmentType={value.type} // eslint-disable-line react/prop-types
|
|
||||||
footnoteId={value.footnoteId} // eslint-disable-line react/prop-types
|
|
||||||
footnoteMarker={value.footnoteMarker} // eslint-disable-line react/prop-types
|
|
||||||
locked={value.locked} // eslint-disable-line react/prop-types
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
headerClassName: 'h5 mb-0',
|
headerClassName: 'h5 mb-0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: `${intl.formatMessage(messages.weight)}`,
|
Header: `${intl.formatMessage(messages.weight)}`,
|
||||||
accessor: 'weight',
|
accessor: 'weight',
|
||||||
headerClassName: 'justify-content-end h5 mb-0',
|
headerClassName: 'justify-content-end h5 mb-0',
|
||||||
// eslint-disable-next-line react/prop-types
|
Cell: ({ value }) => getCell(value.locked, value.weight),
|
||||||
Cell: ({ value }) => (
|
|
||||||
<span className={value.locked ? 'greyed-out' : ''}>{value.weight}</span> // eslint-disable-line react/prop-types
|
|
||||||
),
|
|
||||||
cellClassName: 'text-right small',
|
cellClassName: 'text-right small',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: `${intl.formatMessage(messages.grade)}`,
|
Header: `${intl.formatMessage(messages.grade)}`,
|
||||||
accessor: 'grade',
|
accessor: 'grade',
|
||||||
headerClassName: 'justify-content-end h5 mb-0',
|
headerClassName: 'justify-content-end h5 mb-0',
|
||||||
// eslint-disable-next-line react/prop-types
|
Cell: ({ value }) => getCell(value.locked, value.grade),
|
||||||
Cell: ({ value }) => (
|
|
||||||
<span className={value.locked ? 'greyed-out' : ''}>{value.grade}</span> // eslint-disable-line react/prop-types
|
|
||||||
),
|
|
||||||
cellClassName: 'text-right small',
|
cellClassName: 'text-right small',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: `${intl.formatMessage(messages.weightedGrade)}`,
|
Header: `${intl.formatMessage(messages.weightedGrade)}`,
|
||||||
accessor: 'weightedGrade',
|
accessor: 'weightedGrade',
|
||||||
headerClassName: 'justify-content-end h5 mb-0 text-right',
|
headerClassName: 'justify-content-end h5 mb-0 text-right',
|
||||||
// eslint-disable-next-line react/prop-types
|
Cell: ({ value }) => getCell(value.locked, value.weightedGrade),
|
||||||
Cell: ({ value }) => (
|
|
||||||
<span className={value.locked ? 'greyed-out' : ''}>{value.weightedGrade}</span> // eslint-disable-line react/prop-types
|
|
||||||
),
|
|
||||||
cellClassName: 'text-right font-weight-bold small',
|
cellClassName: 'text-right font-weight-bold small',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -141,7 +134,7 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
GradeSummaryTable.propTypes = {
|
GradeSummaryTable.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function GradeSummaryTableFooter({ intl }) {
|
const GradeSummaryTableFooter = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -34,7 +34,7 @@ function GradeSummaryTableFooter({ intl }) {
|
|||||||
</div>
|
</div>
|
||||||
</DataTable.TableFooter>
|
</DataTable.TableFooter>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
GradeSummaryTableFooter.propTypes = {
|
GradeSummaryTableFooter.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Hyperlink } from '@edx/paragon';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
function RelatedLinks({ intl }) {
|
const RelatedLinks = ({ intl }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -56,7 +56,7 @@ function RelatedLinks({ intl }) {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
RelatedLinks.propTypes = {
|
RelatedLinks.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { resetDeadlines } from '../data';
|
|||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function ShiftDatesAlert({ fetch, intl, model }) {
|
const ShiftDatesAlert = ({ fetch, intl, model }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -29,12 +29,12 @@ function ShiftDatesAlert({ fetch, intl, model }) {
|
|||||||
missedGatedContent,
|
missedGatedContent,
|
||||||
} = datesBannerInfo;
|
} = datesBannerInfo;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
if (!missedDeadlines || missedGatedContent || hasEnded) {
|
if (!missedDeadlines || missedGatedContent || hasEnded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert variant="warning">
|
<Alert variant="warning">
|
||||||
<Row className="w-100 m-0">
|
<Row className="w-100 m-0">
|
||||||
@@ -55,7 +55,7 @@ function ShiftDatesAlert({ fetch, intl, model }) {
|
|||||||
</Row>
|
</Row>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ShiftDatesAlert.propTypes = {
|
ShiftDatesAlert.propTypes = {
|
||||||
fetch: PropTypes.func.isRequired,
|
fetch: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function SuggestedScheduleHeader({ intl }) {
|
const SuggestedScheduleHeader = ({ intl }) => (
|
||||||
return (
|
<p className="large">
|
||||||
<p className="large">
|
{intl.formatMessage(messages.suggestedSchedule)}
|
||||||
{intl.formatMessage(messages.suggestedSchedule)}
|
</p>
|
||||||
</p>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SuggestedScheduleHeader.propTypes = {
|
SuggestedScheduleHeader.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function UpgradeToCompleteAlert({ intl, logUpgradeLinkClick }) {
|
const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -55,7 +55,7 @@ function UpgradeToCompleteAlert({ intl, logUpgradeLinkClick }) {
|
|||||||
</Row>
|
</Row>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
UpgradeToCompleteAlert.propTypes = {
|
UpgradeToCompleteAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function UpgradeToShiftDatesAlert({ intl, logUpgradeLinkClick, model }) {
|
const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -57,7 +57,7 @@ function UpgradeToShiftDatesAlert({ intl, logUpgradeLinkClick, model }) {
|
|||||||
</Row>
|
</Row>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
UpgradeToShiftDatesAlert.propTypes = {
|
UpgradeToShiftDatesAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -6,30 +6,28 @@ import classNames from 'classnames';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import Tabs from '../generic/tabs/Tabs';
|
import Tabs from '../generic/tabs/Tabs';
|
||||||
|
|
||||||
function CourseTabsNavigation({
|
const CourseTabsNavigation = ({
|
||||||
activeTabSlug, className, tabs, intl,
|
activeTabSlug, className, tabs, intl,
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
|
||||||
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
|
<div className="container-xl">
|
||||||
<div className="container-xl">
|
<Tabs
|
||||||
<Tabs
|
className="nav-underline-tabs"
|
||||||
className="nav-underline-tabs"
|
aria-label={intl.formatMessage(messages.courseMaterial)}
|
||||||
aria-label={intl.formatMessage(messages.courseMaterial)}
|
>
|
||||||
>
|
{tabs.map(({ url, title, slug }) => (
|
||||||
{tabs.map(({ url, title, slug }) => (
|
<a
|
||||||
<a
|
key={slug}
|
||||||
key={slug}
|
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
|
||||||
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
|
href={url}
|
||||||
href={url}
|
>
|
||||||
>
|
{title}
|
||||||
{title}
|
</a>
|
||||||
</a>
|
))}
|
||||||
))}
|
</Tabs>
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
|
||||||
CourseTabsNavigation.propTypes = {
|
CourseTabsNavigation.propTypes = {
|
||||||
activeTabSlug: PropTypes.string,
|
activeTabSlug: PropTypes.string,
|
||||||
|
|||||||
@@ -54,32 +54,32 @@ const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequence
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
const checkUnitToSequenceUnitRedirect = memoize((
|
const checkUnitToSequenceUnitRedirect = memoize(
|
||||||
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId,
|
(courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId) => {
|
||||||
) => {
|
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
||||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
if (sequenceMightBeUnit) {
|
||||||
if (sequenceMightBeUnit) {
|
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
|
||||||
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then we need to look up the
|
// we need to look up the correct parent sequence for it, and redirect there.
|
||||||
// correct parent sequence for it, and redirect there.
|
const unitId = sequenceId; // just for clarity during the rest of this method
|
||||||
const unitId = sequenceId; // just for clarity during the rest of this method
|
getSequenceForUnitDeprecated(courseId, unitId).then(
|
||||||
getSequenceForUnitDeprecated(courseId, unitId).then(
|
parentId => {
|
||||||
parentId => {
|
if (parentId) {
|
||||||
if (parentId) {
|
history.replace(`/course/${courseId}/${parentId}/${unitId}`);
|
||||||
history.replace(`/course/${courseId}/${parentId}/${unitId}`);
|
} else {
|
||||||
} else {
|
history.replace(`/course/${courseId}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => { // error case
|
||||||
history.replace(`/course/${courseId}`);
|
history.replace(`/course/${courseId}`);
|
||||||
}
|
},
|
||||||
},
|
);
|
||||||
() => { // error case
|
} else {
|
||||||
history.replace(`/course/${courseId}`);
|
// Invalid sequence that isn't a unit either. Redirect up to main course.
|
||||||
},
|
history.replace(`/course/${courseId}`);
|
||||||
);
|
}
|
||||||
} else {
|
|
||||||
// Invalid sequence that isn't a unit either. Redirect up to main course.
|
|
||||||
history.replace(`/course/${courseId}`);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => {
|
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => {
|
||||||
@@ -225,9 +225,9 @@ class CoursewareContainer extends Component {
|
|||||||
// Check unit to sequence-unit redirect:
|
// Check unit to sequence-unit redirect:
|
||||||
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
|
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
|
||||||
// by filling in the ID of the parent sequence of :unitId.
|
// by filling in the ID of the parent sequence of :unitId.
|
||||||
checkUnitToSequenceUnitRedirect(
|
checkUnitToSequenceUnitRedirect((
|
||||||
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, sectionViaSequenceId, routeUnitId,
|
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, sectionViaSequenceId, routeUnitId
|
||||||
);
|
));
|
||||||
|
|
||||||
// Check sequence to sequence-unit redirect:
|
// Check sequence to sequence-unit redirect:
|
||||||
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
|
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
|
||||||
@@ -255,7 +255,7 @@ class CoursewareContainer extends Component {
|
|||||||
|
|
||||||
this.props.checkBlockCompletion(courseId, sequenceId, routeUnitId);
|
this.props.checkBlockCompletion(courseId, sequenceId, routeUnitId);
|
||||||
history.push(`/course/${courseId}/${sequenceId}/${nextUnitId}`);
|
history.push(`/course/${courseId}/${sequenceId}/${nextUnitId}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleNextSequenceClick = () => {
|
handleNextSequenceClick = () => {
|
||||||
const {
|
const {
|
||||||
@@ -274,14 +274,14 @@ class CoursewareContainer extends Component {
|
|||||||
handleNextSectionCelebration(sequenceId, nextSequence.id);
|
handleNextSectionCelebration(sequenceId, nextSequence.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handlePreviousSequenceClick = () => {
|
handlePreviousSequenceClick = () => {
|
||||||
const { previousSequence, courseId } = this.props;
|
const { previousSequence, courseId } = this.props;
|
||||||
if (previousSequence !== null) {
|
if (previousSequence !== null) {
|
||||||
history.push(`/course/${courseId}/${previousSequence.id}/last`);
|
history.push(`/course/${courseId}/${previousSequence.id}/last`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@@ -320,6 +320,7 @@ const sequenceShape = PropTypes.shape({
|
|||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
unitIds: PropTypes.arrayOf(PropTypes.string),
|
unitIds: PropTypes.arrayOf(PropTypes.string),
|
||||||
sectionId: PropTypes.string.isRequired,
|
sectionId: PropTypes.string.isRequired,
|
||||||
|
saveUnitPosition: PropTypes.any, // eslint-disable-line
|
||||||
});
|
});
|
||||||
|
|
||||||
const sectionShape = PropTypes.shape({
|
const sectionShape = PropTypes.shape({
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user