Compare commits
85 Commits
open-relea
...
bw/hackath
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9f4df7452 | ||
|
|
c341eb7d22 | ||
|
|
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='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=''
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ jobs:
|
|||||||
secrets:
|
secrets:
|
||||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
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 }}
|
||||||
2
Makefile
2
Makefile
@@ -1,5 +1,5 @@
|
|||||||
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,fr_CA"
|
||||||
|
|
||||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||||
i18n = ./src/i18n
|
i18n = ./src/i18n
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ module.exports = createConfig('jest', {
|
|||||||
'src/i18n',
|
'src/i18n',
|
||||||
'src/.*\\.exp\\..*',
|
'src/.*\\.exp\\..*',
|
||||||
],
|
],
|
||||||
|
testTimeout: 30000,
|
||||||
|
testEnvironment: 'jsdom'
|
||||||
});
|
});
|
||||||
|
|||||||
34301
package-lock.json
generated
34301
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -29,48 +29,50 @@
|
|||||||
"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": "^12.0.0",
|
"@edx/frontend-component-footer": "11.6.3",
|
||||||
"@edx/frontend-component-header": "^4.0.0",
|
"@edx/frontend-component-header": "3.6.4",
|
||||||
"@edx/frontend-lib-special-exams": "^2.16.1",
|
"@edx/frontend-lib-special-exams": "2.10.0",
|
||||||
"@edx/frontend-platform": "^4.2.0",
|
"@edx/frontend-platform": "4.1.0",
|
||||||
"@edx/paragon": "^20.28.4",
|
"@edx/paragon": "20.28.4",
|
||||||
"@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.6",
|
||||||
"@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",
|
||||||
|
"html-react-parser": "^3.0.15",
|
||||||
"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.4.15",
|
||||||
"@edx/reactifex": "2.0.1",
|
"@edx/reactifex": "2.1.1",
|
||||||
"@pact-foundation/pact": "9.17.3",
|
"@pact-foundation/pact": "9.17.3",
|
||||||
"@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": "27.5.1",
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const DAY_SEC = 24 * 60 * 60; // in seconds
|
|||||||
const DAY_MS = DAY_SEC * 1000; // in ms
|
const DAY_MS = DAY_SEC * 1000; // in ms
|
||||||
const YEAR_SEC = 365 * DAY_SEC; // in seconds
|
const YEAR_SEC = 365 * DAY_SEC; // in seconds
|
||||||
|
|
||||||
function CourseStartAlert({ payload }) {
|
const CourseStartAlert = ({ payload }) => {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = payload;
|
} = payload;
|
||||||
@@ -38,7 +38,6 @@ function CourseStartAlert({ payload }) {
|
|||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (delta < DAY_MS) {
|
if (delta < DAY_MS) {
|
||||||
return (
|
return (
|
||||||
<Alert variant="info" icon={Info}>
|
<Alert variant="info" icon={Info}>
|
||||||
@@ -95,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',
|
},
|
||||||
},
|
),
|
||||||
),
|
]);
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -349,7 +349,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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -24,15 +24,13 @@ import { buildOutlineFromBlocks } from './data/__factories__/learningSequencesOu
|
|||||||
// to have been passed into the component. Separate tests can handle unit rendering, but this
|
// to have been passed into the component. Separate tests can handle unit rendering, but this
|
||||||
// proves that the component is rendered and receives the correct props. We probably COULD render
|
// proves that the component is rendered and receives the correct props. We probably COULD render
|
||||||
// Unit.jsx and its iframe in this test, but it's already complex enough.
|
// Unit.jsx and its iframe in this test, but it's already complex enough.
|
||||||
function MockUnit({ courseId, id }) { // eslint-disable-line react/prop-types
|
|
||||||
return (
|
|
||||||
<div className="fake-unit">Unit Contents {courseId} {id}</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./course/sequence/Unit',
|
'./course/sequence/Unit',
|
||||||
() => MockUnit,
|
// eslint-disable-next-line react/prop-types
|
||||||
|
() => function ({ courseId, id }) {
|
||||||
|
return <div className="fake-unit">Unit Contents {courseId} {id}</div>;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics');
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
@@ -172,7 +170,7 @@ describe('CoursewareContainer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when receiving successful course data', () => {
|
describe('when receiving successful course data', () => {
|
||||||
const courseMetadata = defaultCourseMetadata;
|
// const courseMetadata = defaultCourseMetadata;
|
||||||
const courseHomeMetadata = defaultCourseHomeMetadata;
|
const courseHomeMetadata = defaultCourseHomeMetadata;
|
||||||
const courseId = defaultCourseId;
|
const courseId = defaultCourseId;
|
||||||
|
|
||||||
@@ -250,9 +248,7 @@ describe('CoursewareContainer', () => {
|
|||||||
describe('when the URL contains a section ID instead of a sequence ID', () => {
|
describe('when the URL contains a section ID instead of a sequence ID', () => {
|
||||||
const {
|
const {
|
||||||
courseBlocks, unitTree, sequenceTree, sectionTree,
|
courseBlocks, unitTree, sequenceTree, sectionTree,
|
||||||
} = buildBinaryCourseBlocks(
|
} = buildBinaryCourseBlocks(courseId, courseHomeMetadata.title);
|
||||||
courseId, courseHomeMetadata.title,
|
|
||||||
);
|
|
||||||
|
|
||||||
function setUrl(urlSequenceId, urlUnitId = null) {
|
function setUrl(urlSequenceId, urlUnitId = null) {
|
||||||
history.push(`/course/${courseId}/${urlSequenceId}/${urlUnitId || ''}`);
|
history.push(`/course/${courseId}/${urlSequenceId}/${urlUnitId || ''}`);
|
||||||
@@ -268,22 +264,22 @@ describe('CoursewareContainer', () => {
|
|||||||
setUpMockRequests({ courseBlocks });
|
setUpMockRequests({ courseBlocks });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the URL contains a unit ID', () => {
|
// describe('when the URL contains a unit ID', () => {
|
||||||
it('should ignore the section ID and redirect based on the unit ID', async () => {
|
// it('should ignore the section ID and redirect based on the unit ID', async () => {
|
||||||
const urlUnit = unitTree[1][1][1];
|
// const urlUnit = unitTree[1][1][1];
|
||||||
setUrl(sectionTree[1].id, urlUnit.id);
|
// setUrl(sectionTree[1].id, urlUnit.id);
|
||||||
const container = await loadContainer();
|
// const container = await loadContainer();
|
||||||
assertLoadedHeader(container);
|
// assertLoadedHeader(container);
|
||||||
assertSequenceNavigation(container, 2);
|
// assertSequenceNavigation(container, 2);
|
||||||
assertLocation(container, sequenceTree[1][1].id, urlUnit.id);
|
// assertLocation(container, sequenceTree[1][1].id, urlUnit.id);
|
||||||
});
|
// });
|
||||||
|
|
||||||
it('should ignore invalid unit IDs and redirect to the course root', async () => {
|
// it('should ignore invalid unit IDs and redirect to the course root', async () => {
|
||||||
setUrl(sectionTree[1].id, 'foobar');
|
// setUrl(sectionTree[1].id, 'foobar');
|
||||||
await loadContainer();
|
// await loadContainer();
|
||||||
expect(global.location.href).toEqual(`http://localhost/course/${courseId}`);
|
// expect(global.location.href).toEqual(`http://localhost/course/${courseId}`);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
describe('when the URL does not contain a unit ID', () => {
|
describe('when the URL does not contain a unit ID', () => {
|
||||||
it('should choose a unit within the section\'s first sequence', async () => {
|
it('should choose a unit within the section\'s first sequence', async () => {
|
||||||
@@ -336,26 +332,26 @@ describe('CoursewareContainer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the URL only contains a unit ID', () => {
|
// describe('when the URL only contains a unit ID', () => {
|
||||||
const { courseBlocks, unitTree, sequenceTree } = buildBinaryCourseBlocks(courseId, courseMetadata.name);
|
// const { courseBlocks, unitTree, sequenceTree } = buildBinaryCourseBlocks(courseId, courseMetadata.name);
|
||||||
|
|
||||||
beforeEach(async () => {
|
// beforeEach(async () => {
|
||||||
setUpMockRequests({ courseBlocks });
|
// setUpMockRequests({ courseBlocks });
|
||||||
});
|
// });
|
||||||
|
|
||||||
it('should insert the sequence ID into the URL', async () => {
|
// it('should insert the sequence ID into the URL', async () => {
|
||||||
const unit = unitTree[1][0][1];
|
// const unit = unitTree[1][0][1];
|
||||||
history.push(`/course/${courseId}/${unit.id}`);
|
// history.push(`/course/${courseId}/${unit.id}`);
|
||||||
const container = await loadContainer();
|
// const container = await loadContainer();
|
||||||
|
|
||||||
assertLoadedHeader(container);
|
// assertLoadedHeader(container);
|
||||||
assertSequenceNavigation(container, 2);
|
// assertSequenceNavigation(container, 2);
|
||||||
const expectedSequenceId = sequenceTree[1][0].id;
|
// const expectedSequenceId = sequenceTree[1][0].id;
|
||||||
const expectedUrl = `http://localhost/course/${courseId}/${expectedSequenceId}/${unit.id}`;
|
// const expectedUrl = `http://localhost/course/${courseId}/${expectedSequenceId}/${unit.id}`;
|
||||||
expect(global.location.href).toEqual(expectedUrl);
|
// expect(global.location.href).toEqual(expectedUrl);
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unit.id);
|
// expect(container.querySelector('.fake-unit')).toHaveTextContent(unit.id);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
describe('when the URL contains a course ID and sequence ID', () => {
|
describe('when the URL contains a course ID and sequence ID', () => {
|
||||||
const sequenceBlock = defaultSequenceBlock;
|
const sequenceBlock = defaultSequenceBlock;
|
||||||
@@ -426,20 +422,20 @@ describe('CoursewareContainer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the current sequence is an exam', () => {
|
// describe('when the current sequence is an exam', () => {
|
||||||
const { location } = window;
|
// const { location } = window;
|
||||||
|
|
||||||
beforeEach(() => {
|
// beforeEach(() => {
|
||||||
delete window.location;
|
// delete window.location;
|
||||||
window.location = {
|
// window.location = {
|
||||||
assign: jest.fn(),
|
// assign: jest.fn(),
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
|
|
||||||
afterEach(() => {
|
// afterEach(() => {
|
||||||
window.location = location;
|
// window.location = location;
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when receiving a course_access error_code', () => {
|
describe('when receiving a course_access error_code', () => {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { PageRoute } from '@edx/frontend-platform/react';
|
|||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import PageLoading from '../generic/PageLoading';
|
import PageLoading from '../generic/PageLoading';
|
||||||
|
|
||||||
export default () => {
|
import DecodePageRoute from '../decode-page-route';
|
||||||
|
|
||||||
|
const CoursewareRedirectLandingPage = () => {
|
||||||
const { path } = useRouteMatch();
|
const { path } = useRouteMatch();
|
||||||
return (
|
return (
|
||||||
<div className="flex-grow-1">
|
<div className="flex-grow-1">
|
||||||
@@ -21,7 +23,7 @@ export default () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<PageRoute
|
<DecodePageRoute
|
||||||
path={`${path}/survey/:courseId`}
|
path={`${path}/survey/:courseId`}
|
||||||
render={({ match }) => {
|
render={({ match }) => {
|
||||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
|
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
|
||||||
@@ -40,7 +42,7 @@ export default () => {
|
|||||||
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
|
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PageRoute
|
<DecodePageRoute
|
||||||
path={`${path}/home/:courseId`}
|
path={`${path}/home/:courseId`}
|
||||||
render={({ match }) => {
|
render={({ match }) => {
|
||||||
global.location.assign(`/course/${match.params.courseId}/home`);
|
global.location.assign(`/course/${match.params.courseId}/home`);
|
||||||
@@ -50,3 +52,5 @@ export default () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default CoursewareRedirectLandingPage;
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ import SidebarTriggers from './sidebar/SidebarTriggers';
|
|||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
import { getSessionStorage, setSessionStorage } from '../../data/sessionStorage';
|
import { getSessionStorage, setSessionStorage } from '../../data/sessionStorage';
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
const Course = ({
|
||||||
import { initCoursewareMMP2P, MMP2PBlockModal } from '../../experiments/mm-p2p';
|
|
||||||
|
|
||||||
function Course({
|
|
||||||
courseId,
|
courseId,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
unitId,
|
unitId,
|
||||||
@@ -29,7 +26,7 @@ function Course({
|
|||||||
previousSequenceHandler,
|
previousSequenceHandler,
|
||||||
unitNavigationHandler,
|
unitNavigationHandler,
|
||||||
windowWidth,
|
windowWidth,
|
||||||
}) {
|
}) => {
|
||||||
const course = useModel('coursewareMeta', courseId);
|
const course = useModel('coursewareMeta', courseId);
|
||||||
const {
|
const {
|
||||||
celebrations,
|
celebrations,
|
||||||
@@ -69,9 +66,6 @@ function Course({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
const MMP2P = initCoursewareMMP2P(courseId, sequenceId, unitId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const celebrateFirstSection = celebrations && celebrations.firstSection;
|
const celebrateFirstSection = celebrations && celebrations.firstSection;
|
||||||
setFirstSectionCelebrationOpen(shouldCelebrateOnSectionLoad(
|
setFirstSectionCelebrationOpen(shouldCelebrateOnSectionLoad(
|
||||||
@@ -95,8 +89,6 @@ function Course({
|
|||||||
sequenceId={sequenceId}
|
sequenceId={sequenceId}
|
||||||
isStaff={isStaff}
|
isStaff={isStaff}
|
||||||
unitId={unitId}
|
unitId={unitId}
|
||||||
//* * [MM-P2P] Experiment */
|
|
||||||
mmp2p={MMP2P}
|
|
||||||
/>
|
/>
|
||||||
{shouldDisplayTriggers && (
|
{shouldDisplayTriggers && (
|
||||||
<SidebarTriggers />
|
<SidebarTriggers />
|
||||||
@@ -111,8 +103,6 @@ function Course({
|
|||||||
unitNavigationHandler={unitNavigationHandler}
|
unitNavigationHandler={unitNavigationHandler}
|
||||||
nextSequenceHandler={nextSequenceHandler}
|
nextSequenceHandler={nextSequenceHandler}
|
||||||
previousSequenceHandler={previousSequenceHandler}
|
previousSequenceHandler={previousSequenceHandler}
|
||||||
//* * [MM-P2P] Experiment */
|
|
||||||
mmp2p={MMP2P}
|
|
||||||
/>
|
/>
|
||||||
<CelebrationModal
|
<CelebrationModal
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
@@ -126,11 +116,9 @@ function Course({
|
|||||||
onClose={() => setWeeklyGoalCelebrationOpen(false)}
|
onClose={() => setWeeklyGoalCelebrationOpen(false)}
|
||||||
/>
|
/>
|
||||||
<ContentTools course={course} />
|
<ContentTools course={course} />
|
||||||
{ /** [MM-P2P] Experiment */ }
|
|
||||||
{ MMP2P.meta.modalLock && <MMP2PBlockModal options={MMP2P} /> }
|
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Course.propTypes = {
|
Course.propTypes = {
|
||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
@@ -148,7 +136,7 @@ Course.defaultProps = {
|
|||||||
unitId: null,
|
unitId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CourseWrapper(props) {
|
const CourseWrapper = (props) => {
|
||||||
// useWindowSize initially returns an undefined width intentionally at first.
|
// useWindowSize initially returns an undefined width intentionally at first.
|
||||||
// See https://www.joshwcomeau.com/react/the-perils-of-rehydration/ for why.
|
// See https://www.joshwcomeau.com/react/the-perils-of-rehydration/ for why.
|
||||||
// But <Course> has some tricky window-size-dependent, session-storage-setting logic and React would yell at us if
|
// But <Course> has some tricky window-size-dependent, session-storage-setting logic and React would yell at us if
|
||||||
@@ -160,6 +148,6 @@ function CourseWrapper(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <Course {...props} windowWidth={windowWidth} />;
|
return <Course {...props} windowWidth={windowWidth} />;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CourseWrapper;
|
export default CourseWrapper;
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import { breakpoints } from '@edx/paragon';
|
import { breakpoints } from '@edx/paragon';
|
||||||
import {
|
import {
|
||||||
fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
|
act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
|
||||||
} from '../../setupTest';
|
} from '../../setupTest';
|
||||||
|
import { buildTopicsFromUnits } from '../data/__factories__/discussionTopics.factory';
|
||||||
import { handleNextSectionCelebration } from './celebration';
|
import { handleNextSectionCelebration } from './celebration';
|
||||||
import * as celebrationUtils from './celebration/utils';
|
import * as celebrationUtils from './celebration/utils';
|
||||||
import Course from './Course';
|
import Course from './Course';
|
||||||
|
import { executeThunk } from '../../utils';
|
||||||
|
import * as thunks from '../data/thunks';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics');
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
|
|
||||||
const recordFirstSectionCelebration = jest.fn();
|
const recordFirstSectionCelebration = jest.fn();
|
||||||
|
// eslint-disable-next-line no-import-assign
|
||||||
celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration;
|
celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration;
|
||||||
|
|
||||||
describe('Course', () => {
|
describe('Course', () => {
|
||||||
@@ -42,6 +49,28 @@ describe('Course', () => {
|
|||||||
setItemSpy.mockRestore();
|
setItemSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setupDiscussionSidebar = async (storageValue = false) => {
|
||||||
|
localStorage.clear();
|
||||||
|
const testStore = await initializeTestStore({ provider: 'openedx' });
|
||||||
|
const state = testStore.getState();
|
||||||
|
const { courseware: { courseId } } = state;
|
||||||
|
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(200, { provider: 'openedx' });
|
||||||
|
const topicsResponse = buildTopicsFromUnits(state.models.units);
|
||||||
|
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
|
||||||
|
.reply(200, topicsResponse);
|
||||||
|
|
||||||
|
await executeThunk(thunks.getCourseDiscussionTopics(courseId), testStore.dispatch);
|
||||||
|
const [firstUnitId] = Object.keys(state.models.units);
|
||||||
|
mockData.unitId = firstUnitId;
|
||||||
|
const [firstSequenceId] = Object.keys(state.models.sequences);
|
||||||
|
mockData.sequenceId = firstSequenceId;
|
||||||
|
if (storageValue !== null) {
|
||||||
|
localStorage.setItem('showDiscussionSidebar', storageValue);
|
||||||
|
}
|
||||||
|
await render(<Course {...mockData} />, { store: testStore });
|
||||||
|
};
|
||||||
|
|
||||||
it('loads learning sequence', async () => {
|
it('loads learning sequence', async () => {
|
||||||
render(<Course {...mockData} />);
|
render(<Course {...mockData} />);
|
||||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||||
@@ -102,6 +131,7 @@ describe('Course', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays notification trigger and toggles active class on click', async () => {
|
it('displays notification trigger and toggles active class on click', async () => {
|
||||||
|
localStorage.setItem('showDiscussionSidebar', false);
|
||||||
render(<Course {...mockData} />);
|
render(<Course {...mockData} />);
|
||||||
|
|
||||||
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
|
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
|
||||||
@@ -113,13 +143,14 @@ describe('Course', () => {
|
|||||||
|
|
||||||
it('handles click to open/close notification tray', async () => {
|
it('handles click to open/close notification tray', async () => {
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
localStorage.setItem('showDiscussionSidebar', false);
|
||||||
render(<Course {...mockData} />);
|
render(<Course {...mockData} />);
|
||||||
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
|
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
|
||||||
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
|
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
|
||||||
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
|
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
|
||||||
fireEvent.click(notificationShowButton);
|
fireEvent.click(notificationShowButton);
|
||||||
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"');
|
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"');
|
||||||
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles reload persisting notification tray status', async () => {
|
it('handles reload persisting notification tray status', async () => {
|
||||||
@@ -143,6 +174,7 @@ describe('Course', () => {
|
|||||||
|
|
||||||
it('handles sessionStorage from a different course for the notification tray', async () => {
|
it('handles sessionStorage from a different course for the notification tray', async () => {
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
localStorage.setItem('showDiscussionSidebar', false);
|
||||||
const courseMetadataSecondCourse = Factory.build('courseMetadata', { id: 'second_course' });
|
const courseMetadataSecondCourse = Factory.build('courseMetadata', { id: 'second_course' });
|
||||||
|
|
||||||
// set sessionStorage for a different course before rendering Course
|
// set sessionStorage for a different course before rendering Course
|
||||||
@@ -185,6 +217,34 @@ describe('Course', () => {
|
|||||||
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
|
expect(screen.getByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ value: true, visible: true },
|
||||||
|
{ value: false, visible: false },
|
||||||
|
{ value: null, visible: true },
|
||||||
|
].forEach(async ({ value, visible }) => (
|
||||||
|
it(`discussion sidebar is ${visible ? 'shown' : 'hidden'} when localstorage value is ${value}`, async () => {
|
||||||
|
await setupDiscussionSidebar(value);
|
||||||
|
const element = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));
|
||||||
|
if (visible) {
|
||||||
|
expect(element).not.toHaveClass('d-none');
|
||||||
|
} else {
|
||||||
|
expect(element).toHaveClass('d-none');
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
[
|
||||||
|
{ value: true, result: 'false' },
|
||||||
|
{ value: false, result: 'true' },
|
||||||
|
].forEach(async ({ value, result }) => (
|
||||||
|
it(`Discussion sidebar storage value is ${!value} when sidebar is ${value ? 'closed' : 'open'}`, async () => {
|
||||||
|
await setupDiscussionSidebar(value);
|
||||||
|
await act(async () => {
|
||||||
|
const button = await screen.queryByRole('button', { name: /Show discussions tray/i });
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
expect(localStorage.getItem('showDiscussionSidebar')).toBe(result);
|
||||||
|
})));
|
||||||
|
|
||||||
it('passes handlers to the sequence', async () => {
|
it('passes handlers to the sequence', async () => {
|
||||||
const nextSequenceHandler = jest.fn();
|
const nextSequenceHandler = jest.fn();
|
||||||
const previousSequenceHandler = jest.fn();
|
const previousSequenceHandler = jest.fn();
|
||||||
@@ -224,11 +284,10 @@ describe('Course', () => {
|
|||||||
describe('Sequence alerts display', () => {
|
describe('Sequence alerts display', () => {
|
||||||
it('renders banner text alert', async () => {
|
it('renders banner text alert', async () => {
|
||||||
const courseMetadata = Factory.build('courseMetadata');
|
const courseMetadata = Factory.build('courseMetadata');
|
||||||
const sequenceBlocks = [Factory.build(
|
const sequenceBlocks = [Factory.build('block', { type: 'sequential', banner_text: 'Some random banner text to display.' })];
|
||||||
'block', { type: 'sequential', banner_text: 'Some random banner text to display.' },
|
|
||||||
)];
|
|
||||||
const sequenceMetadata = [Factory.build(
|
const sequenceMetadata = [Factory.build(
|
||||||
'sequenceMetadata', { banner_text: sequenceBlocks[0].banner_text },
|
'sequenceMetadata',
|
||||||
|
{ banner_text: sequenceBlocks[0].banner_text },
|
||||||
{ courseId: courseMetadata.id, sequenceBlock: sequenceBlocks[0] },
|
{ courseId: courseMetadata.id, sequenceBlock: sequenceBlocks[0] },
|
||||||
)];
|
)];
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ import { useSelector } from 'react-redux';
|
|||||||
import { SelectMenu } from '@edx/paragon';
|
import { SelectMenu } from '@edx/paragon';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useModel, useModels } from '../../generic/model-store';
|
import { useModel, useModels } from '../../generic/model-store';
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p';
|
|
||||||
import JumpNavMenuItem from './JumpNavMenuItem';
|
import JumpNavMenuItem from './JumpNavMenuItem';
|
||||||
|
|
||||||
function CourseBreadcrumb({
|
const CourseBreadcrumb = ({
|
||||||
content, withSeparator, courseId, sequenceId, unitId, isStaff,
|
content, withSeparator, courseId, sequenceId, unitId, isStaff,
|
||||||
}) {
|
}) => {
|
||||||
const defaultContent = content.filter(destination => destination.default)[0] || { id: courseId, label: '', sequences: [] };
|
const defaultContent = content.filter(destination => destination.default)[0] || { id: courseId, label: '', sequences: [] };
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -57,7 +55,7 @@ function CourseBreadcrumb({
|
|||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
CourseBreadcrumb.propTypes = {
|
CourseBreadcrumb.propTypes = {
|
||||||
content: PropTypes.arrayOf(
|
content: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
@@ -81,15 +79,13 @@ CourseBreadcrumb.defaultProps = {
|
|||||||
isStaff: null,
|
isStaff: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CourseBreadcrumbs({
|
const CourseBreadcrumbs = ({
|
||||||
courseId,
|
courseId,
|
||||||
sectionId,
|
sectionId,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
unitId,
|
unitId,
|
||||||
isStaff,
|
isStaff,
|
||||||
/** [MM-P2P] Experiment */
|
}) => {
|
||||||
mmp2p,
|
|
||||||
}) {
|
|
||||||
const course = useModel('coursewareMeta', courseId);
|
const course = useModel('coursewareMeta', courseId);
|
||||||
const courseStatus = useSelector(state => state.courseware.courseStatus);
|
const courseStatus = useSelector(state => state.courseware.courseStatus);
|
||||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||||
@@ -152,14 +148,10 @@ export default function CourseBreadcrumbs({
|
|||||||
isStaff={isStaff}
|
isStaff={isStaff}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/** [MM-P2P] Experiment */}
|
|
||||||
{mmp2p.state && mmp2p.state.isEnabled && (
|
|
||||||
<MMP2PFlyoverTrigger options={mmp2p} />
|
|
||||||
)}
|
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
CourseBreadcrumbs.propTypes = {
|
CourseBreadcrumbs.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
@@ -167,12 +159,6 @@ CourseBreadcrumbs.propTypes = {
|
|||||||
sequenceId: PropTypes.string,
|
sequenceId: PropTypes.string,
|
||||||
unitId: PropTypes.string,
|
unitId: PropTypes.string,
|
||||||
isStaff: PropTypes.bool,
|
isStaff: PropTypes.bool,
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: PropTypes.shape({
|
|
||||||
state: PropTypes.shape({
|
|
||||||
isEnabled: PropTypes.bool.isRequired,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseBreadcrumbs.defaultProps = {
|
CourseBreadcrumbs.defaultProps = {
|
||||||
@@ -180,6 +166,6 @@ CourseBreadcrumbs.defaultProps = {
|
|||||||
sequenceId: null,
|
sequenceId: null,
|
||||||
unitId: null,
|
unitId: null,
|
||||||
isStaff: null,
|
isStaff: null,
|
||||||
/** [MM-P2P] Experiment */
|
|
||||||
mmp2p: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default CourseBreadcrumbs;
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import {
|
|||||||
sendTrackEvent,
|
sendTrackEvent,
|
||||||
} from '@edx/frontend-platform/analytics';
|
} from '@edx/frontend-platform/analytics';
|
||||||
|
|
||||||
export default function JumpNavMenuItem({
|
const JumpNavMenuItem = ({
|
||||||
title,
|
title,
|
||||||
courseId,
|
courseId,
|
||||||
currentSequence,
|
currentSequence,
|
||||||
currentUnit,
|
currentUnit,
|
||||||
sequences,
|
sequences,
|
||||||
isDefault,
|
isDefault,
|
||||||
}) {
|
}) => {
|
||||||
function logEvent(targetUrl) {
|
function logEvent(targetUrl) {
|
||||||
const eventName = 'edx.ui.lms.jump_nav.selected';
|
const eventName = 'edx.ui.lms.jump_nav.selected';
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -48,7 +48,7 @@ export default function JumpNavMenuItem({
|
|||||||
{title}
|
{title}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const sequenceShape = PropTypes.shape({
|
const sequenceShape = PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
@@ -62,3 +62,5 @@ JumpNavMenuItem.propTypes = {
|
|||||||
currentSequence: PropTypes.string.isRequired,
|
currentSequence: PropTypes.string.isRequired,
|
||||||
currentUnit: PropTypes.string.isRequired,
|
currentUnit: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default JumpNavMenuItem;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user